Skip to content

Commit

Permalink
Refactor issuer into smaller components
Browse files Browse the repository at this point in the history
Issuer now accepts checked data. Introduced helper functions which
creates this checked data. Components can be tested separately.
  • Loading branch information
stevenvegt committed Dec 10, 2024
1 parent ff91b48 commit 6e2e1f5
Show file tree
Hide file tree
Showing 3 changed files with 362 additions and 275 deletions.
26 changes: 25 additions & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ func main() {
fmt.Println(err)
os.Exit(-1)
}
fmt.Println("VC result:")
err = printLineAndFlush(jwt)
if err != nil {
fmt.Println(err)
Expand Down Expand Up @@ -123,5 +124,28 @@ func printLineAndFlush(jwt string) error {
}

func issueVc(vc VC) (string, error) {
return uzi_vc_issuer.Issue(vc.CertificateFile, vc.SigningKey, vc.SubjectDID, vc.Test, vc.IncludePermanent, vc.SubjectAttributes)
chain, err := uzi_vc_issuer.NewValidCertificateChain(vc.CertificateFile)
if err != nil {
return "", err
}

key, err := uzi_vc_issuer.NewPrivateKey(vc.SigningKey)
if err != nil {
return "", err
}

subject, err := uzi_vc_issuer.NewSubjectDID(vc.SubjectDID)
if err != nil {
return "", err
}

credential, err := uzi_vc_issuer.Issue(chain, key, subject,
uzi_vc_issuer.SubjectAttributes(vc.SubjectAttributes...),
uzi_vc_issuer.AllowTestUraCa(vc.Test))

if err != nil {
return "", err
}

return credential.Raw(), nil
}
240 changes: 185 additions & 55 deletions uzi_vc_issuer/ura_issuer.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,92 +9,205 @@ import (
"encoding/pem"
"errors"
"fmt"
"github.com/nuts-foundation/go-did/did"
"os"
"time"

"github.com/nuts-foundation/go-did/did"

"github.com/google/uuid"
"github.com/lestrrat-go/jwx/v2/cert"
"github.com/lestrrat-go/jwx/v2/jwa"
"github.com/lestrrat-go/jwx/v2/jws"
"github.com/lestrrat-go/jwx/v2/jwt"
ssi "github.com/nuts-foundation/go-did"
"github.com/nuts-foundation/go-did/vc"
"github.com/nuts-foundation/uzi-did-x509-issuer/ca_certs"
"github.com/nuts-foundation/uzi-did-x509-issuer/did_x509"
pem2 "github.com/nuts-foundation/uzi-did-x509-issuer/pem"
"github.com/nuts-foundation/uzi-did-x509-issuer/uzi_vc_validator"
"github.com/nuts-foundation/uzi-did-x509-issuer/x509_cert"
)

// Issue generates a URA Verifiable Credential using provided certificate, signing key, subject DID, and subject name.
func Issue(certificateFile string, signingKeyFile string, subjectDID string, allowTestUraCa bool, includePermanentIdentifier bool, subjectAttributes []x509_cert.SubjectTypeName) (string, error) {
pemBlocks, err := pem2.ParseFileOrPath(certificateFile, "CERTIFICATE")
// filename represents a valid file name. The file must exist.
type fileName string

// nonEmptyBytes represents a non-empty byte slice.
type nonEmptyBytes []byte

// newFileName creates a new fileName from a string. It returns an error if the file does not exist.
func newFileName(name string) (fileName, error) {
if _, err := os.Stat(name); err != nil {
return fileName(""), err
}

return fileName(name), nil
}

// readFile reads a file and returns its content as nonEmptyBytes. It returns an error if the file does not exist or is empty.
func readFile(name fileName) (nonEmptyBytes, error) {
bytes, err := os.ReadFile(string(name))
if err != nil {
return "", err
return nil, err
}
if len(bytes) == 0 {
return nil, errors.New("file is empty")
}
allowSelfSignedCa := len(pemBlocks) > 1
if len(pemBlocks) == 1 {
certificate := pemBlocks[0]
pemBlocks, err = ca_certs.GetDERs(allowTestUraCa)
return nonEmptyBytes(bytes), nil
}

// pemBlocks represents a list of one or more PEM blocks.
type pemBlocks []*pem.Block

// parsePemBytes parses a nonEmptyBytes slice into a pemBlocks
// it returns an error if the input does not contain any PEM blocks.
func parsePemBytes(f nonEmptyBytes) (pemBlocks, error) {
blocks := make([]*pem.Block, 0)
for {
block, rest := pem.Decode(f)
if block == nil {
break
}
blocks = append(blocks, block)
f = rest
}

if len(blocks) == 0 {
return nil, errors.New("no PEM blocks found")
}

return blocks, nil
}

// parseCertificatesFromPemBlocks parses a list of PEM blocks into a list of x509.Certificate instances.
// It returns an error if any of the blocks cannot be parsed into a certificate.
func parseCertificatesFromPemBlocks(blocks pemBlocks) (certificateList, error) {
certs := make([]*x509.Certificate, 0)
for _, block := range blocks {
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return "", err
return nil, err
}
pemBlocks = append(pemBlocks, certificate)
certs = append(certs, cert)
}
return certs, nil
}

// certificateList represents a non empty slice of x509.Certificate instances.
type certificateList []*x509.Certificate

// validCertificateChain represents a valid certificate chain.
type validCertificateChain certificateList
type privateKey *rsa.PrivateKey
type subjectDID string

// issueOptions contains values for options for issuing a UZI VC.
type issueOptions struct {
allowTestUraCa bool
includePermanentIdentifier bool
subjectAttributes []x509_cert.SubjectTypeName
}

// Option is an interface for a function in the options pattern.
type Option = func(*issueOptions)

// X509Credential represents a JWT encoded X.509 credential.
type X509Credential string

var defaultIssueOptions = &issueOptions{
allowTestUraCa: false,
includePermanentIdentifier: false,
subjectAttributes: []x509_cert.SubjectTypeName{},
}

func NewValidCertificateChain(fileName string) (validCertificateChain, error) {
certFileName, err := newFileName(fileName)

signingKeys, err := pem2.ParseFileOrPath(signingKeyFile, "PRIVATE KEY")
if err != nil {
return "", err
return nil, err
}
if len(signingKeys) == 0 {
err := fmt.Errorf("no signing keys found")
return "", err

fileBytes, err := readFile(certFileName)
if err != nil {
return nil, err
}
privateKey, err := x509_cert.ParsePrivateKey(signingKeys[0])
pemBlocks, err := parsePemBytes(fileBytes)
if err != nil {
return "", err
return nil, err
}

certs, err := x509_cert.ParseCertificates(pemBlocks)
certs, err := parseCertificatesFromPemBlocks(pemBlocks)
if err != nil {
return "", err
return nil, err
}

chain, err := BuildCertificateChain(certs)
chain, err := newCertificateChain(certs)
if err != nil {
return "", err
return nil, err
}
err = validateChain(chain)

return chain, nil
}

func NewPrivateKey(fileName string) (privateKey, error) {
keyFileName, err := newFileName(fileName)
if err != nil {
return "", err
return nil, err
}
types := []x509_cert.SanTypeName{x509_cert.SanTypeOtherName}
if includePermanentIdentifier {
types = append(types, x509_cert.SanTypePermanentIdentifierValue)
types = append(types, x509_cert.SanTypePermanentIdentifierAssigner)

keyFileBytes, err := readFile(keyFileName)
if err != nil {
return nil, err
}
credential, err := BuildUraVerifiableCredential(chain, privateKey, subjectDID, subjectAttributes, types...)

keyBlocks, err := parsePemBytes(keyFileBytes)
if err != nil {
return "", err
return nil, err
}

key, err := newRSAPrivateKey(keyBlocks)
if err != nil {
return nil, err
}

return key, nil
}

func NewSubjectDID(did string) (subjectDID, error) {
return subjectDID(did), nil
}

// newRSAPrivateKey parses a DER-encoded private key into an *rsa.PrivateKey.
// It returns an error if the key is not in PKCS8 format or not an RSA key.
func newRSAPrivateKey(pemBlocks pemBlocks) (privateKey, error) {
if len(pemBlocks) != 1 || pemBlocks[0].Type != "PRIVATE KEY" {
return nil, errors.New("expected exactly one private key block")
}
jwtString := credential.Raw()
validator := uzi_vc_validator.NewUraValidator(allowTestUraCa, allowSelfSignedCa)
err = validator.Validate(jwtString)
block := pemBlocks[0]

key, err := x509.ParsePKCS8PrivateKey(block.Bytes)
if err != nil {
return "", err
key, err = x509.ParsePKCS1PrivateKey(block.Bytes)
if err != nil {
return nil, err
}
}

if _, ok := key.(*rsa.PrivateKey); !ok {
return nil, fmt.Errorf("key is not RSA")
}
return jwtString, nil
return key.(*rsa.PrivateKey), err
}

// BuildUraVerifiableCredential constructs a verifiable credential with specified certificates, signing key, subject DID.
func BuildUraVerifiableCredential(chain []*x509.Certificate, signingKey *rsa.PrivateKey, subjectDID string, subjectAttributes []x509_cert.SubjectTypeName, types ...x509_cert.SanTypeName) (*vc.VerifiableCredential, error) {
if len(chain) == 0 {
return nil, errors.New("empty certificate chain")
func Issue(chain validCertificateChain, key privateKey, subject subjectDID, optionFns ...Option) (*vc.VerifiableCredential, error) {
options := defaultIssueOptions
for _, fn := range optionFns {
fn(options)
}
if signingKey == nil {
return nil, errors.New("signing key is nil")

types := []x509_cert.SanTypeName{x509_cert.SanTypeOtherName}
if options.includePermanentIdentifier {
types = append(types, x509_cert.SanTypePermanentIdentifierValue)
types = append(types, x509_cert.SanTypePermanentIdentifierAssigner)
}
did, err := did_x509.CreateDid(chain[0], chain[len(chain)-1], subjectAttributes, types...)

did, err := did_x509.CreateDid(chain[0], chain[len(chain)-1], options.subjectAttributes, types...)
if err != nil {
return nil, err
}
Expand All @@ -108,7 +221,7 @@ func BuildUraVerifiableCredential(chain []*x509.Certificate, signingKey *rsa.Pri
if err != nil {
return nil, err
}
subjectTypes, err := x509_cert.SelectSubjectTypes(signingCert, subjectAttributes...)
subjectTypes, err := x509_cert.SelectSubjectTypes(signingCert, options.subjectAttributes...)
if err != nil {
return nil, err
}
Expand All @@ -120,11 +233,11 @@ func BuildUraVerifiableCredential(chain []*x509.Certificate, signingKey *rsa.Pri
if uzi != serialNumber {
return nil, errors.New("serial number does not match UZI number")
}
template, err := uraCredential(did, signingCert.NotAfter, otherNameValues, subjectTypes, subjectDID)
template, err := uraCredential(did, signingCert.NotAfter, otherNameValues, subjectTypes, subject)
if err != nil {
return nil, err
}
credential, err := vc.CreateJWTVerifiableCredential(context.Background(), *template, func(ctx context.Context, claims map[string]interface{}, headers map[string]interface{}) (string, error) {
return vc.CreateJWTVerifiableCredential(context.Background(), *template, func(ctx context.Context, claims map[string]interface{}, headers map[string]interface{}) (string, error) {
token, err := convertClaims(claims)
if err != nil {
return "", err
Expand Down Expand Up @@ -158,13 +271,30 @@ func BuildUraVerifiableCredential(chain []*x509.Certificate, signingKey *rsa.Pri
return "", err
}

sign, err := jwt.Sign(token, jwt.WithKey(jwa.PS512, signingKey, jws.WithProtectedHeaders(hdrs)))
sign, err := jwt.Sign(token, jwt.WithKey(jwa.PS512, rsa.PrivateKey(*key), jws.WithProtectedHeaders(hdrs)))
return string(sign), err
})
if err != nil {
return nil, err
}

// AllowTestUraCa allows the use of Test URA server certificates.
func AllowTestUraCa(allow bool) Option {
return func(o *issueOptions) {
o.allowTestUraCa = allow
}
}

// IncludePermanentIdentifier includes the permanent identifier in the UZI VC.
func IncludePermanentIdentifier(include bool) Option {
return func(o *issueOptions) {
o.includePermanentIdentifier = include
}
}

// SubjectAttributes sets the subject attributes to include in the UZI VC.
func SubjectAttributes(attributes ...x509_cert.SubjectTypeName) Option {
return func(o *issueOptions) {
o.subjectAttributes = attributes
}
return credential, nil
}

// marshalChain converts a slice of x509.Certificate instances to a cert.Chain, encoding each certificate as PEM.
Expand Down Expand Up @@ -199,11 +329,11 @@ func validateChain(certs []*x509.Certificate) error {
return errors.New("failed to find a path to the root certificate in the chain, are you using a (Test) URA server certificate (Hint: the --test mode is required for Test URA server certificates)")
}

// BuildCertificateChain constructs a certificate chain from a given list of certificates and a starting signing certificate.
// newCertificateChain constructs a valid certificate chain from a given list of certificates and a starting signing certificate.
// It recursively finds parent certificates for non-root CAs and appends them to the chain.
// It assumes the list might not be in order.
// The returning chain contains the signing cert at the start and the root cert at the end.
func BuildCertificateChain(certs []*x509.Certificate) ([]*x509.Certificate, error) {
func newCertificateChain(certs certificateList) (validCertificateChain, error) {
var signingCert *x509.Certificate
for _, c := range certs {
if c != nil && !c.IsCA {
Expand Down Expand Up @@ -265,7 +395,7 @@ func convertHeaders(headers map[string]interface{}) (jws.Headers, error) {

// uraCredential generates a VerifiableCredential for a given URA and UZI number, including the subject's DID.
// It sets a 1-year expiration period from the current issuance date.
func uraCredential(issuer string, expirationDate time.Time, otherNameValues []*x509_cert.OtherNameValue, subjectTypes []*x509_cert.SubjectValue, subjectDID string) (*vc.VerifiableCredential, error) {
func uraCredential(issuer string, expirationDate time.Time, otherNameValues []*x509_cert.OtherNameValue, subjectTypes []*x509_cert.SubjectValue, subjectDID subjectDID) (*vc.VerifiableCredential, error) {
iat := time.Now()
subject := map[string]interface{}{
"id": subjectDID,
Expand Down
Loading

0 comments on commit 6e2e1f5

Please sign in to comment.