ccadb2OneCRL/onecrl/onecrl.go (189 lines of code) (raw):
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package onecrl
import (
"fmt"
"github.com/mozilla/OneCRL-Tools/ccadb2OneCRL/set"
log "github.com/sirupsen/logrus"
"github.com/mozilla/OneCRL-Tools/ccadb2OneCRL/ccadb"
"github.com/pkg/errors"
"github.com/mozilla/OneCRL-Tools/ccadb2OneCRL/utils"
"github.com/mozilla/OneCRL-Tools/kinto/api"
"github.com/mozilla/OneCRL-Tools/kinto/api/buckets"
"github.com/mozilla/OneCRL-Tools/kinto/api/collections"
)
func NewOneCRL() *OneCRL {
return &OneCRL{
Collection: collections.NewCollection(buckets.NewBucket("security-state-staging"), "onecrl"),
Data: []*Record{},
}
}
type OneCRL struct {
*collections.Collection `json:"-"`
Data []*Record `json:"data"`
}
type Record struct {
// It is rather awkward to hold onto a pointer to the associated CCADB entry,
// however it makes constructing a Comparison struct much easier in main
// as you can bundle the two together as soon has you find the match.
// However, this could be a good opportunity for refactoring/decoupling.
CCADB *ccadb.Certificate `json:"-"`
Schema int `json:"schema"`
Details Details `json:"details"`
Enabled bool `json:"enabled"`
IssuerName string `json:"issuerName,omitempty"`
SerialNumber string `json:"serialNumber,omitempty"`
Subject string `json:"subject,omitempty"`
PubKeyHash string `json:"pubKeyHash,omitempty"`
*api.Record
}
type Details struct {
Bug string `json:"bug"`
Who string `json:"who"`
Why string `json:"why"`
Name string `json:"name"`
Created string `json:"created"`
}
func (r *Record) Type() set.Type {
if r.PubKeyHash != "" && r.Subject != "" {
return set.SubjectKeyHashType
} else if r.IssuerName != "" && r.SerialNumber != "" {
return set.IssuerSerialType
}
log.WithField("entry", r).Panic("a OneCRL entry was found that does not appear to be either a " +
"SubjectPubKeyHash type nor the more common IssuerSerial type")
// The Go compiler understands that the above statement panics the program and compiles just fine, but Goland
// will complain endlessly (as of Aug 2020) that this method will not compile due to a missing return.
return set.IssuerSerialType
}
// IssuerSerial parses the X.509 certificate retrieved from the CCADB,
// extracts the issuer (https://tools.ietf.org/html/rfc5280#section-4.1.2.4) and
// serial number (https://tools.ietf.org/html/rfc5280#section-4.1.2.2)
//
// An error will be logged and a nil IssuerSerial will be returned if the issuer field could not be
// parsed or the serial number could not be b64 decoded.
func (r *Record) IssuerSerial() *set.IssuerSerial {
issuer, err := r.parseIssuer()
if err != nil {
log.WithError(err).
WithField("record", r).
Warn("failed to parse an issuer field from OneCRL")
return nil
}
// Decoding and re-encoding the string coerces everyone to the same b64 standard.
// That is, those without padding get forced into having padding.
serial, err := utils.B64Decode(r.SerialNumber)
if err != nil {
log.WithError(err).
WithField("record", r).
Warn("OneCRL serial base64 serial decode error")
return nil
}
is := set.NewIssuerSerial(issuer, serial)
return &is
}
// SubjectKeyHash parses the subject (https://tools.ietf.org/html/rfc5280#section-4.1.2.6)
// field of a OneCRL entry.
//
// An error will be logged and a nil SubjectKeyHash will be returned if the subject field could not be
// parsed or the public key hash could not be b64 decoded.
func (r *Record) SubjectKeyHash() *set.SubjectKeyHash {
subject, err := r.parseSubject()
if err != nil {
log.WithError(err).
WithField("record", r).
Warn("failed to parse an subject field from OneCRL")
return nil
}
// Decoding and re-encoding the string coerces everyone to the same b64 standard.
// That is, those without padding get forced into having padding.
hash, err := utils.B64Decode(r.PubKeyHash)
if err != nil {
log.WithError(err).
WithField("record", r).
Warn("OneCRL serial base64 key hash decode error")
return nil
}
skh := set.NewSubjectKeyHash(subject, hash)
return &skh
}
func (r *Record) parseSubject() ([]byte, error) {
if r.Type() != set.SubjectKeyHashType {
return nil, fmt.Errorf("attempted to parse a subject from a non SubjectPubKeyHash onecrl entry, got %d", r.Type())
}
subject, err := unbase64RawDistinguishedName(r.Subject)
if err != nil {
return nil, err
}
return subject, nil
}
func (r *Record) parseIssuer() ([]byte, error) {
if r.Type() != set.IssuerSerialType {
return nil, fmt.Errorf("attempted to parse an issuer from a non IssuerSerial onecrl entry, got %d", r.Type())
}
issuer, err := unbase64RawDistinguishedName(r.IssuerName)
if err != nil {
return nil, err
}
return issuer, nil
}
// A Comparison holds the same piece of information in the preferred
// representation of OneCRL and CCADB. The purpose is to facilitate
// quick left/right comparisons between the datasets.
//
// E.G. OneCRL encodes a serial number as a base64 however the CCADB
// encodes it as an uppercase hexadecimal.
type Comparison struct {
OneCRL string
CCADB string
}
type IssuerSerialComparison struct {
Issuer Comparison `json:"issuer"`
Serial Comparison `json:"serial"`
}
type SubjectKeyHashComparison struct {
Subject Comparison `json:"subject"`
Keyhash Comparison `json:"keyHash"`
}
// ToComparison generates a comparison between OneCRL and
// CCADB that easy for a human to read in a left/right
// sort of way.
//
// Example object may be:
//
// {
// "issuer": {
// "OneCRL": "MFAxJDAiBgNVBAsTG0dsb2JhbFNpZ24gRUNDIFJvb3QgQ0EgLSBSNTETMBEGA1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbg==",
// "CCADB": "GlobalSign"
// },
// "serial": {
// "OneCRL": "Ae5fInnr9AhpWVIjkw==",
// "CCADB": "01EE5F2279EBF4086959522393"
// }
// }
func (r *Record) ToComparison() (interface{}, error) {
cert, err := r.CCADB.ParseCertificate()
if err != nil {
return nil, fmt.Errorf("failed to parse the certificate bundled with the CCADB when building a comparison "+
"between the CCADB and OneCRL, err: %v", err)
}
switch r.Type() {
case set.IssuerSerialType:
return IssuerSerialComparison{
Issuer: Comparison{
OneCRL: r.IssuerName,
CCADB: cert.Issuer.String(),
},
Serial: Comparison{
OneCRL: r.SerialNumber,
CCADB: r.CCADB.CertificateSerialNumber,
},
}, nil
case set.SubjectKeyHashType:
raw, err := utils.B64Decode(r.PubKeyHash)
if err != nil {
return nil, err
}
return SubjectKeyHashComparison{
Subject: Comparison{
OneCRL: r.Subject,
CCADB: cert.Subject.String(),
},
Keyhash: Comparison{
OneCRL: r.PubKeyHash,
CCADB: fmt.Sprintf("%X", raw),
},
}, nil
default:
log.Panic("non-exhaustive switch")
return nil, nil
}
}
// FromCCADB constructs a new OneCRL Record from the provided
// CCADB certificate.
//
// The outcome of this procedure ultimately is what becomes
// the proposed changed to OneCRL.
func FromCCADB(c *ccadb.Certificate) (*Record, error) {
cert, err := c.ParseCertificate()
if err != nil {
return nil, err
}
serial, err := utils.RawSerialBytes(cert.RawTBSCertificate)
if err != nil {
return nil, err
}
record := &Record{
CCADB: c,
Details: Details{
Bug: "",
Who: "",
Why: "",
Name: "",
Created: "",
},
Enabled: false,
IssuerName: utils.B64Encode(cert.RawIssuer),
SerialNumber: utils.B64Encode(serial),
Subject: "",
PubKeyHash: "",
}
return record, nil
}
func unbase64RawDistinguishedName(rdns string) ([]byte, error) {
i, err := utils.B64Decode(rdns)
if err != nil {
return nil, errors.Wrap(err, "OneCRL RDNS b64 decode error")
}
return i, nil
}