diff --git a/test/pki/test.go b/test/pki/test.go index 7c5cc8c35a..5fe4162d34 100644 --- a/test/pki/test.go +++ b/test/pki/test.go @@ -19,13 +19,22 @@ package pki import ( + "crypto/rand" + "crypto/rsa" "crypto/tls" "crypto/x509" + "crypto/x509/pkix" _ "embed" + "encoding/asn1" + "encoding/base64" + "github.com/lestrrat-go/jwx/v2/cert" "github.com/nuts-foundation/nuts-node/test/io" + "math/big" + "net" "os" "path" "testing" + "time" ) // CertificateData contains the PEM-encoded test certificate and its key. @@ -58,11 +67,6 @@ func InvalidCertificate() tls.Certificate { return cert } -// InvalidCertificateFile returns the path to a file containing an invalid test certificate and its key. -func InvalidCertificateFile(t *testing.T) string { - return writeToTemp(t, "invalid-cert.pem", InvalidCertificateData) -} - // Certificate returns a valid test certificate. func Certificate() tls.Certificate { cert, err := tls.X509KeyPair(CertificateData, CertificateData) @@ -96,3 +100,237 @@ func writeToTemp(t *testing.T, fileName string, data []byte) string { } return filePath } + +func CertsToChain(certs []*x509.Certificate) *cert.Chain { + result := new(cert.Chain) + for _, c := range certs { + _ = result.Add([]byte(base64.StdEncoding.EncodeToString(c.Raw))) + } + return result +} + +// BuildCertChain generates a certificate chain, including root, intermediate, and signing certificates. +func BuildCertChain(identifiers []string, subjectSerialNumber string) ([]*x509.Certificate, []*rsa.PrivateKey, error) { + rootKey, rootCert, err := BuildRootCert() + if err != nil { + return nil, nil, err + } + intermediateL1Key, intermediateL1Cert, err := buildIntermediateCert(rootCert, rootKey, "Intermediate CA Level 1") + if err != nil { + return nil, nil, err + } + intermediateL2Key, intermediateL2Cert, err := buildIntermediateCert(intermediateL1Cert, intermediateL1Key, "Intermediate CA Level 2") + if err != nil { + return nil, nil, err + } + if subjectSerialNumber == "" { + subjectSerialNumber = "32121323" + } + signingKey, signingCert, err := BuildSigningCert(identifiers, intermediateL2Cert, intermediateL2Key, subjectSerialNumber) + if err != nil { + return nil, nil, err + } + return []*x509.Certificate{ + signingCert, + intermediateL2Cert, + intermediateL1Cert, + rootCert, + }, []*rsa.PrivateKey{ + signingKey, + intermediateL2Key, + intermediateL1Key, + rootKey, + }, nil +} + +func BuildSigningCert(identifiers []string, intermediateL2Cert *x509.Certificate, intermediateL2Key *rsa.PrivateKey, serialNumber string) (*rsa.PrivateKey, *x509.Certificate, error) { + signingKey, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + return nil, nil, err + } + signingTmpl, err := signingCertTemplate(nil, identifiers) + if err != nil { + return nil, nil, err + } + signingTmpl.Subject.SerialNumber = serialNumber + signingCert, err := createCert(signingTmpl, intermediateL2Cert, &signingKey.PublicKey, intermediateL2Key) + if err != nil { + return nil, nil, err + } + return signingKey, signingCert, err +} + +func buildIntermediateCert(parentCert *x509.Certificate, parentKey *rsa.PrivateKey, subjectName string) (*rsa.PrivateKey, *x509.Certificate, error) { + intermediateL1Key, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + return nil, nil, err + } + intermediateL1Tmpl, err := certTemplate(subjectName) + if err != nil { + return nil, nil, err + } + intermediateL1Cert, err := createCert(intermediateL1Tmpl, parentCert, &intermediateL1Key.PublicKey, parentKey) + if err != nil { + return nil, nil, err + } + return intermediateL1Key, intermediateL1Cert, nil +} + +func BuildRootCert() (*rsa.PrivateKey, *x509.Certificate, error) { + rootKey, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + return nil, nil, err + } + rootCertTmpl, err := certTemplate("Root CA") + if err != nil { + return nil, nil, err + } + rootCert, err := createCert(rootCertTmpl, rootCertTmpl, &rootKey.PublicKey, rootKey) + if err != nil { + return nil, nil, err + } + return rootKey, rootCert, nil +} + +// certTemplate generates a template for a x509 certificate with a given serial number. If no serial number is provided, a random one is generated. +// The certificate is valid for one month and uses SHA256 with RSA for the signature algorithm. +func certTemplate(subjectName string) (*x509.Certificate, error) { + serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 8) + serialNumber, _ := rand.Int(rand.Reader, serialNumberLimit) + tmpl := x509.Certificate{ + SerialNumber: serialNumber, + Subject: pkix.Name{Organization: []string{subjectName}}, + SignatureAlgorithm: x509.SHA256WithRSA, + NotBefore: time.Now(), + NotAfter: time.Now().Add(time.Hour * 24 * 30), // valid for a month + BasicConstraintsValid: true, + } + tmpl.IsCA = true + tmpl.KeyUsage = x509.KeyUsageCertSign | x509.KeyUsageDigitalSignature + tmpl.ExtKeyUsage = []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth} + return &tmpl, nil +} + +// createCert generates a new x509 certificate using the provided template and parent certificates, public and private keys. +// It returns the generated certificate, its PEM-encoded version, and any error encountered during the process. +func createCert(template, parent *x509.Certificate, pub interface{}, parentPriv interface{}) (cert *x509.Certificate, err error) { + certDER, err := x509.CreateCertificate(rand.Reader, template, parent, pub, parentPriv) + if err != nil { + return nil, err + } + // parse the resulting certificate so we can use it again + cert, err = x509.ParseCertificate(certDER) + if err != nil { + return nil, err + } + return cert, err +} + +// signingCertTemplate creates a x509.Certificate template for a signing certificate with an optional serial number. +func signingCertTemplate(serialNumber *big.Int, identifiers []string) (*x509.Certificate, error) { + // generate a random serial number (a real cert authority would have some logic behind this) + if serialNumber == nil { + serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 8) + serialNumber, _ = rand.Int(rand.Reader, serialNumberLimit) + } + + tmpl := x509.Certificate{ + SignatureAlgorithm: x509.SHA256WithRSA, + SerialNumber: serialNumber, + Subject: pkix.Name{ + Organization: []string{"NUTS Foundation"}, + CommonName: "www.example.com", + Country: []string{"NL"}, + Locality: []string{"Amsterdam", "The Hague"}, + OrganizationalUnit: []string{"The A-Team"}, + StreetAddress: []string{"Amsterdamseweg 100"}, + PostalCode: []string{"1011 NL"}, + Province: []string{"Noord-Holland"}, + }, + NotBefore: time.Now(), + NotAfter: time.Now().Add(time.Hour * 24 * 30), // valid for a month + } + tmpl.KeyUsage = x509.KeyUsageDigitalSignature + tmpl.ExtKeyUsage = []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth} + // Either the ExtraExtensions SubjectAlternativeNameType is set, or the Subject Alternate Name values are set, + // both don't mix + if len(identifiers) > 0 { + err := addCertSan(&tmpl, identifiers, "testhost.example.com") + if err != nil { + return nil, err + } + } else { + tmpl.DNSNames = []string{"www.example.com", "example.com"} + tmpl.EmailAddresses = []string{"info@example.com", "no-reply@example.org"} + tmpl.IPAddresses = []net.IP{net.ParseIP("192.1.2.3"), net.ParseIP("192.1.2.4")} + } + return &tmpl, nil +} + +func addCertSan(tmpl *x509.Certificate, identifiers []string, altHostName string) error { + // OtherName represents a structure for other name in ASN.1 + type OtherName struct { + TypeID asn1.ObjectIdentifier + Value asn1.RawValue `asn1:"tag:0,explicit"` + } + var ( + // SubjectAlternativeNameType defines the OID for Subject Alternative Name + SubjectAlternativeNameType = asn1.ObjectIdentifier{2, 5, 29, 17} + // OtherNameType defines the OID for Other Name + OtherNameType = asn1.ObjectIdentifier{2, 5, 5, 5} + ) + + var list []asn1.RawValue + // Add the alternative host name first + value, err := toRawValue(altHostName, "tag:2") + if err != nil { + return err + } + list = append(list, *value) + + for _, identifier := range identifiers { + raw, err := toRawValue(identifier, "ia5") + if err != nil { + return err + } + otherName := OtherName{ + TypeID: OtherNameType, + Value: asn1.RawValue{ + Class: 2, + Tag: 0, + IsCompound: true, + Bytes: raw.FullBytes, + }, + } + + raw, err = toRawValue(otherName, "tag:0") + if err != nil { + return err + } + list = append(list, *raw) + } + marshal, err := asn1.Marshal(list) + if err != nil { + return err + } + tmpl.ExtraExtensions = append(tmpl.ExtraExtensions, pkix.Extension{ + Id: SubjectAlternativeNameType, + Critical: false, + Value: marshal, + }) + return nil +} + +// toRawValue marshals an ASN.1 identifier with a given tag, then unmarshals it into a RawValue structure. +func toRawValue(value any, tag string) (*asn1.RawValue, error) { + b, err := asn1.MarshalWithParams(value, tag) + if err != nil { + return nil, err + } + var val asn1.RawValue + _, err = asn1.Unmarshal(b, &val) + if err != nil { + return nil, err + } + return &val, nil +} diff --git a/vcr/verifier/signature_verifier_test.go b/vcr/verifier/signature_verifier_test.go index 64b4044980..3f6c81b306 100644 --- a/vcr/verifier/signature_verifier_test.go +++ b/vcr/verifier/signature_verifier_test.go @@ -28,19 +28,16 @@ import ( "crypto/sha1" "crypto/sha256" "crypto/x509" - "crypto/x509/pkix" "encoding/base64" "encoding/json" - "encoding/pem" "errors" "github.com/google/uuid" "github.com/lestrrat-go/jwx/v2/cert" "github.com/lestrrat-go/jwx/v2/jwa" "github.com/nuts-foundation/nuts-node/pki" + testpki "github.com/nuts-foundation/nuts-node/test/pki" "github.com/nuts-foundation/nuts-node/vdr/didx509" - "math/big" "os" - "strings" "testing" "time" @@ -91,10 +88,14 @@ func TestSignatureVerifier_VerifySignature(t *testing.T) { t.Run("JWT - X509", func(t *testing.T) { ura := "312312312" - chainPems, rootCert, signingKey, signingCert, err := buildCertChain(ura) + certs, keys, err := testpki.BuildCertChain(nil, ura) + chain := testpki.CertsToChain(certs) + signingCert := certs[0] + signingKey := keys[0] + rootCert := certs[len(certs)-1] assert.NoError(t, err) - cred, err := buildX509Credential(chainPems, signingCert, rootCert, signingKey, ura) + cred, err := buildX509Credential(chain, signingCert, rootCert, signingKey, ura) assert.NoError(t, err) t.Run("happy flow", func(t *testing.T) { @@ -116,7 +117,7 @@ func TestSignatureVerifier_VerifySignature(t *testing.T) { assert.ErrorIs(t, err, expectedError) }) t.Run("wrong ura", func(t *testing.T) { - cred, err := buildX509Credential(chainPems, signingCert, rootCert, signingKey, ura) + cred, err := buildX509Credential(chain, signingCert, rootCert, signingKey, ura) assert.NoError(t, err) sv, validator := x509VerifierTestSetup(t) expectedError := errors.New("wrong ura") @@ -310,9 +311,9 @@ func TestSignatureVerifier_VerifySignature(t *testing.T) { }) } -func buildX509Credential(chainPems *cert.Chain, signingCert *x509.Certificate, rootCert *x509.Certificate, signingKey *rsa.PrivateKey, ura string) (*vc.VerifiableCredential, error) { +func buildX509Credential(chain *cert.Chain, signingCert *x509.Certificate, rootCert *x509.Certificate, signingKey *rsa.PrivateKey, ura string) (*vc.VerifiableCredential, error) { headers := map[string]interface{}{} - headers["x5c"] = chainPems + headers["x5c"] = chain hashSha1 := sha1.Sum(signingCert.Raw) headers["x5t"] = base64.RawURLEncoding.EncodeToString(hashSha1[:]) @@ -343,83 +344,6 @@ func buildX509Credential(chainPems *cert.Chain, signingCert *x509.Certificate, r return cred, nil } -func buildCertChain(ura string) (*cert.Chain, *x509.Certificate, *rsa.PrivateKey, *x509.Certificate, error) { - rootKey, err := rsa.GenerateKey(rand.Reader, 2048) - if err != nil { - return nil, nil, nil, nil, err - } - rootCertTmpl, err := CertTemplate("Root CA") - if err != nil { - return nil, nil, nil, nil, err - } - rootCertTmpl.IsCA = true - rootCertTmpl.KeyUsage = x509.KeyUsageCertSign | x509.KeyUsageDigitalSignature - rootCertTmpl.ExtKeyUsage = []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth} - rootCert, rootPem, err := CreateCert(rootCertTmpl, rootCertTmpl, &rootKey.PublicKey, rootKey) - if err != nil { - return nil, nil, nil, nil, err - } - - intermediateL1Key, err := rsa.GenerateKey(rand.Reader, 2048) - if err != nil { - return nil, nil, nil, nil, err - } - intermediateL1Tmpl, err := CertTemplate("Intermediate CA Level 1") - if err != nil { - return nil, nil, nil, nil, err - } - intermediateL1Tmpl.IsCA = true - intermediateL1Tmpl.KeyUsage = x509.KeyUsageCertSign | x509.KeyUsageDigitalSignature - intermediateL1Tmpl.ExtKeyUsage = []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth} - intermediateL1Cert, intermediateL1Pem, err := CreateCert(intermediateL1Tmpl, rootCertTmpl, &intermediateL1Key.PublicKey, rootKey) - if err != nil { - return nil, nil, nil, nil, err - } - - intermediateL2Key, err := rsa.GenerateKey(rand.Reader, 2048) - if err != nil { - return nil, nil, nil, nil, err - } - intermediateL2Tmpl, err := CertTemplate("Intermediate CA Level 2") - if err != nil { - return nil, nil, nil, nil, err - } - intermediateL2Tmpl.IsCA = true - intermediateL2Tmpl.KeyUsage = x509.KeyUsageCertSign | x509.KeyUsageDigitalSignature - intermediateL2Tmpl.ExtKeyUsage = []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth} - intermediateL2Cert, intermediateL2Pem, err := CreateCert(intermediateL2Tmpl, intermediateL1Cert, &intermediateL2Key.PublicKey, intermediateL1Key) - if err != nil { - return nil, nil, nil, nil, err - } - - signingKey, err := rsa.GenerateKey(rand.Reader, 2048) - if err != nil { - return nil, nil, nil, nil, err - } - signingTmpl, err := CertTemplate("Leaf") - if err != nil { - return nil, nil, nil, nil, err - } - signingTmpl.Subject.SerialNumber = ura - signingTmpl.KeyUsage = x509.KeyUsageDigitalSignature - signingTmpl.ExtKeyUsage = []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth} - signingCert, signingPEM, err := CreateCert(signingTmpl, intermediateL2Cert, &signingKey.PublicKey, intermediateL2Key) - if err != nil { - return nil, nil, nil, nil, err - } - - certChain := &cert.Chain{} - for _, str := range []string{signingPEM, intermediateL2Pem, intermediateL1Pem, rootPem} { - fixedPem := strings.ReplaceAll(str, "\n", "\\n") - err = certChain.Add([]byte(fixedPem)) - if err != nil { - return nil, nil, nil, nil, err - } - } - - return certChain, rootCert, signingKey, signingCert, nil -} - func testUraCredential(did string, ura string) (*vc.VerifiableCredential, error) { credential := &vc.VerifiableCredential{} credential.Issuer = ssi.MustParseURI(did) @@ -437,19 +361,6 @@ func testUraCredential(did string, ura string) (*vc.VerifiableCredential, error) return credential, nil } -func fixChainHeaders(chain *cert.Chain) (*cert.Chain, error) { - rv := &cert.Chain{} - for i := 0; i < chain.Len(); i++ { - value, _ := chain.Get(i) - der := strings.ReplaceAll(string(value), "\n", "\\n") - err := rv.AddString(der) - if err != nil { - return nil, err - } - } - return rv, nil -} - func signatureVerifierTestSetup(t testing.TB) (signatureVerifier, *resolver.MockKeyResolver) { ctrl := gomock.NewController(t) keyResolver := resolver.NewMockKeyResolver(ctrl) @@ -470,36 +381,3 @@ func x509VerifierTestSetup(t testing.TB) (signatureVerifier, *pki.MockValidator) jsonldManager: jsonld.NewTestJSONLDManager(t), }, pkiMock } - -// CertTemplate is a helper function to create a cert template with a serial number and other required fields -func CertTemplate(subjectName string) (*x509.Certificate, error) { - // generate a random serial number (a real cert authority would have some logic behind this) - serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 8) - serialNumber, _ := rand.Int(rand.Reader, serialNumberLimit) - tmpl := x509.Certificate{ - SerialNumber: serialNumber, - Subject: pkix.Name{Organization: []string{subjectName}}, - SignatureAlgorithm: x509.SHA256WithRSA, - NotBefore: time.Now(), - NotAfter: time.Now().Add(time.Hour * 24 * 30), // valid for a month - BasicConstraintsValid: true, - } - return &tmpl, nil -} - -// CreateCert invokes x509.CreateCertificate and returns it in the x509.Certificate format -func CreateCert(template, parent *x509.Certificate, pub interface{}, parentPriv interface{}) (cert *x509.Certificate, certPEM string, err error) { - certDER, err := x509.CreateCertificate(rand.Reader, template, parent, pub, parentPriv) - if err != nil { - return nil, "", err - } - // parse the resulting certificate so we can use it again - cert, err = x509.ParseCertificate(certDER) - if err != nil { - return nil, "", err - } - // PEM encode the certificate (this is a standard TLS encoding) - b := pem.Block{Type: "CERTIFICATE", Bytes: certDER} - certPEM = string(pem.EncodeToMemory(&b)) - return cert, certPEM, err -} diff --git a/vcr/verifier/verifier_test.go b/vcr/verifier/verifier_test.go index 7050a5054d..70fad6b8fe 100644 --- a/vcr/verifier/verifier_test.go +++ b/vcr/verifier/verifier_test.go @@ -24,6 +24,7 @@ import ( "encoding/json" "errors" "github.com/nuts-foundation/nuts-node/storage/orm" + "github.com/nuts-foundation/nuts-node/test/pki" "net/http" "net/http/httptest" "os" @@ -306,11 +307,15 @@ func TestVerifier_Verify(t *testing.T) { t.Run("verify x509", func(t *testing.T) { ura := "312312312" - chainPems, rootCert, signingKey, signingCert, err := buildCertChain(ura) + certs, keys, err := pki.BuildCertChain(nil, ura) + chain := pki.CertsToChain(certs) + signingCert := certs[0] + signingKey := keys[0] + rootCert := certs[len(certs)-1] assert.NoError(t, err) t.Run("ok", func(t *testing.T) { - cred, err := buildX509Credential(chainPems, signingCert, rootCert, signingKey, ura) + cred, err := buildX509Credential(chain, signingCert, rootCert, signingKey, ura) assert.NoError(t, err) ctx := newMockContext(t) ctx.store.EXPECT().GetRevocations(*cred.ID).Return(nil, ErrNotFound) @@ -324,7 +329,7 @@ func TestVerifier_Verify(t *testing.T) { assert.NoError(t, err) }) t.Run("ok revoked", func(t *testing.T) { - cred, err := buildX509Credential(chainPems, signingCert, rootCert, signingKey, ura) + cred, err := buildX509Credential(chain, signingCert, rootCert, signingKey, ura) assert.NoError(t, err) ctx := newMockContext(t) ctx.store.EXPECT().GetRevocations(*cred.ID) @@ -336,7 +341,7 @@ func TestVerifier_Verify(t *testing.T) { assert.EqualError(t, err, "credential is revoked") }) t.Run("untrusted", func(t *testing.T) { - cred, err := buildX509Credential(chainPems, signingCert, rootCert, signingKey, ura) + cred, err := buildX509Credential(chain, signingCert, rootCert, signingKey, ura) assert.NoError(t, err) ctx := newMockContext(t) ctx.store.EXPECT().GetRevocations(*cred.ID).Return(nil, ErrNotFound) @@ -345,7 +350,7 @@ func TestVerifier_Verify(t *testing.T) { assert.EqualError(t, err, "credential issuer is untrusted") }) t.Run("expired", func(t *testing.T) { - cred, err := buildX509Credential(chainPems, signingCert, rootCert, signingKey, ura) + cred, err := buildX509Credential(chain, signingCert, rootCert, signingKey, ura) assert.NoError(t, err) ctx := newMockContext(t) ctx.store.EXPECT().GetRevocations(*cred.ID).Return(nil, ErrNotFound) @@ -357,7 +362,7 @@ func TestVerifier_Verify(t *testing.T) { assert.EqualError(t, err, "credential not valid at given time") }) t.Run("broken headers", func(t *testing.T) { - cred, err := buildX509Credential(chainPems, signingCert, rootCert, signingKey, ura) + cred, err := buildX509Credential(chain, signingCert, rootCert, signingKey, ura) assert.NoError(t, err) ctx := newMockContext(t) ctx.store.EXPECT().GetRevocations(*cred.ID).Return(nil, ErrNotFound) diff --git a/vdr/didx509/resolver.go b/vdr/didx509/resolver.go index 95447d8c81..b0feacd63a 100644 --- a/vdr/didx509/resolver.go +++ b/vdr/didx509/resolver.go @@ -105,7 +105,7 @@ func (r Resolver) Resolve(id did.DID, metadata *resolver.ResolveMetadata) (*did. } chain, err := parseChain(chainHeader) if err != nil { - return nil, nil, err + return nil, nil, fmt.Errorf("did:x509 x5c certificate parsing: %w", err) } _, err = findCertificateByHash(chain, ref.RootCertRef, ref.Method) if err != nil { diff --git a/vdr/didx509/resolver_test.go b/vdr/didx509/resolver_test.go index cac6441331..aa259938d0 100644 --- a/vdr/didx509/resolver_test.go +++ b/vdr/didx509/resolver_test.go @@ -22,14 +22,14 @@ import ( "crypto/sha1" "crypto/sha512" "encoding/base64" - "encoding/pem" "errors" "fmt" "github.com/lestrrat-go/jwx/v2/cert" "github.com/minio/sha256-simd" "github.com/nuts-foundation/go-did/did" "github.com/nuts-foundation/nuts-node/pki" - resolver2 "github.com/nuts-foundation/nuts-node/vdr/resolver" + testpki "github.com/nuts-foundation/nuts-node/test/pki" + "github.com/nuts-foundation/nuts-node/vdr/resolver" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/mock/gomock" @@ -40,24 +40,26 @@ import ( func TestManager_Resolve_OtherName(t *testing.T) { ctrl := gomock.NewController(t) validator := pki.NewMockValidator(ctrl) - resolver := NewResolver(validator) - metadata := resolver2.ResolveMetadata{} + didResolver := NewResolver(validator) + metadata := resolver.ResolveMetadata{} otherNameValue := "A_BIG_STRING" otherNameValueSecondary := "A_SECOND_STRING" - _, certChain, rootCertificate, _, signingCert, err := BuildCertChain([]string{otherNameValue, otherNameValueSecondary}) + certs, _, err := testpki.BuildCertChain([]string{otherNameValue, otherNameValueSecondary}, "") require.NoError(t, err) + signingCert := leafCertFromCerts(certs) + rootCertificate := rootCertFromCerts(certs) metadata.JwtProtectedHeaders = make(map[string]interface{}) - metadata.JwtProtectedHeaders[X509CertChainHeader] = certChain - metadata.JwtProtectedHeaders[X509CertThumbprintHeader] = sha1Sum(signingCert.Raw) - metadata.JwtProtectedHeaders[X509CertThumbprintS256Header] = sha256Sum(signingCert.Raw) + metadata.JwtProtectedHeaders[X509CertChainHeader] = testpki.CertsToChain(certs) + metadata.JwtProtectedHeaders[X509CertThumbprintHeader] = sha1Sum(leafCertFromCerts(certs).Raw) + metadata.JwtProtectedHeaders[X509CertThumbprintS256Header] = sha256Sum(leafCertFromCerts(certs).Raw) - rootDID := did.MustParseDID(fmt.Sprintf("did:x509:0:%s:%s::san:otherName:%s", "sha256", sha256Sum(rootCertificate.Raw), otherNameValue)) + rootDID := did.MustParseDID(fmt.Sprintf("did:x509:0:%s:%s::san:otherName:%s", "sha256", sha256Sum(rootCertFromCerts(certs).Raw), otherNameValue)) t.Run("test nulls", func(t *testing.T) { chain, _ := metadata.GetProtectedHeaderChain(X509CertChainHeader) delete(metadata.JwtProtectedHeaders, X509CertChainHeader) - _, _, err := resolver.Resolve(rootDID, &metadata) + _, _, err := didResolver.Resolve(rootDID, &metadata) require.Error(t, err) assert.Equal(t, ErrX509ChainMissing.Error(), err.Error()) metadata.JwtProtectedHeaders[X509CertChainHeader] = chain @@ -66,7 +68,7 @@ func TestManager_Resolve_OtherName(t *testing.T) { t.Run("test x5c cast issue", func(t *testing.T) { chain, _ := metadata.GetProtectedHeaderChain(X509CertChainHeader) metadata.JwtProtectedHeaders[X509CertChainHeader] = "GARBAGE" - _, _, err := resolver.Resolve(rootDID, &metadata) + _, _, err := didResolver.Resolve(rootDID, &metadata) require.Error(t, err) assert.Equal(t, ErrX509ChainMissing.Error(), err.Error()) metadata.JwtProtectedHeaders[X509CertChainHeader] = chain @@ -75,7 +77,7 @@ func TestManager_Resolve_OtherName(t *testing.T) { t.Run("happy flow, policy depth of 0", func(t *testing.T) { rootDID := did.MustParseDID(fmt.Sprintf("did:x509:0:%s:%s", "sha256", sha256Sum(rootCertificate.Raw))) validator.EXPECT().ValidateStrict(gomock.Any()) - resolve, documentMetadata, err := resolver.Resolve(rootDID, &metadata) + resolve, documentMetadata, err := didResolver.Resolve(rootDID, &metadata) require.NoError(t, err) assert.NotNil(t, resolve) @@ -87,7 +89,7 @@ func TestManager_Resolve_OtherName(t *testing.T) { }) t.Run("happy flow, policy depth of 1 and primary value", func(t *testing.T) { validator.EXPECT().ValidateStrict(gomock.Any()) - resolve, documentMetadata, err := resolver.Resolve(rootDID, &metadata) + resolve, documentMetadata, err := didResolver.Resolve(rootDID, &metadata) require.NoError(t, err) assert.NotNil(t, resolve) @@ -101,7 +103,7 @@ func TestManager_Resolve_OtherName(t *testing.T) { rootDID := did.MustParseDID(fmt.Sprintf("did:x509:0:%s:%s::san:otherName:%s", "sha256", sha256Sum(rootCertificate.Raw), otherNameValueSecondary)) validator.EXPECT().ValidateStrict(gomock.Any()) - resolve, documentMetadata, err := resolver.Resolve(rootDID, &metadata) + resolve, documentMetadata, err := didResolver.Resolve(rootDID, &metadata) require.NoError(t, err) assert.NotNil(t, resolve) @@ -115,7 +117,7 @@ func TestManager_Resolve_OtherName(t *testing.T) { rootDID := did.MustParseDID(fmt.Sprintf("did:x509:0:%s:%s::san:otherName:%s::subject:OU:%s", "sha256", sha256Sum(rootCertificate.Raw), otherNameValue, "The%20A-Team")) validator.EXPECT().ValidateStrict(gomock.Any()) - resolve, documentMetadata, err := resolver.Resolve(rootDID, &metadata) + resolve, documentMetadata, err := didResolver.Resolve(rootDID, &metadata) require.NoError(t, err) assert.NotNil(t, resolve) @@ -129,7 +131,7 @@ func TestManager_Resolve_OtherName(t *testing.T) { rootDID := did.MustParseDID(fmt.Sprintf("did:x509:0:%s:%s::san:otherName:%s::san:otherName:%s", "sha256", sha256Sum(rootCertificate.Raw), otherNameValue, otherNameValueSecondary)) validator.EXPECT().ValidateStrict(gomock.Any()) - resolve, documentMetadata, err := resolver.Resolve(rootDID, &metadata) + resolve, documentMetadata, err := didResolver.Resolve(rootDID, &metadata) require.NoError(t, err) assert.NotNil(t, resolve) @@ -143,7 +145,7 @@ func TestManager_Resolve_OtherName(t *testing.T) { rootDID := did.MustParseDID(fmt.Sprintf("did:x509:0:%s:%s::san:otherName:%s::san:otherName:%s", "sha256", sha256Sum(rootCertificate.Raw), otherNameValue, otherNameValueSecondary)) validator.EXPECT().ValidateStrict(gomock.Any()) - resolve, documentMetadata, err := resolver.Resolve(rootDID, &metadata) + resolve, documentMetadata, err := didResolver.Resolve(rootDID, &metadata) require.NoError(t, err) assert.NotNil(t, resolve) @@ -156,7 +158,7 @@ func TestManager_Resolve_OtherName(t *testing.T) { t.Run("happy flow with only x5t header", func(t *testing.T) { delete(metadata.JwtProtectedHeaders, X509CertThumbprintS256Header) validator.EXPECT().ValidateStrict(gomock.Any()) - resolve, documentMetadata, err := resolver.Resolve(rootDID, &metadata) + resolve, documentMetadata, err := didResolver.Resolve(rootDID, &metadata) require.NoError(t, err) assert.NotNil(t, resolve) assert.NotNil(t, documentMetadata) @@ -165,7 +167,7 @@ func TestManager_Resolve_OtherName(t *testing.T) { t.Run("happy flow with only x5t#S256 header", func(t *testing.T) { delete(metadata.JwtProtectedHeaders, X509CertThumbprintHeader) validator.EXPECT().ValidateStrict(gomock.Any()) - resolve, documentMetadata, err := resolver.Resolve(rootDID, &metadata) + resolve, documentMetadata, err := didResolver.Resolve(rootDID, &metadata) require.NoError(t, err) assert.NotNil(t, resolve) assert.NotNil(t, documentMetadata) @@ -175,7 +177,7 @@ func TestManager_Resolve_OtherName(t *testing.T) { expectedErr := ErrNoCertsInHeaders delete(metadata.JwtProtectedHeaders, X509CertThumbprintHeader) delete(metadata.JwtProtectedHeaders, X509CertThumbprintS256Header) - _, _, err := resolver.Resolve(rootDID, &metadata) + _, _, err := didResolver.Resolve(rootDID, &metadata) require.Error(t, err) assert.ErrorIs(t, expectedErr, err) metadata.JwtProtectedHeaders[X509CertThumbprintHeader] = sha1Sum(signingCert.Raw) @@ -185,7 +187,7 @@ func TestManager_Resolve_OtherName(t *testing.T) { rootDID := did.MustParseDID(fmt.Sprintf("did:x509:0:%s:%s::san:otherName:%s", "sha512", sha512Sum(rootCertificate.Raw), otherNameValue)) delete(metadata.JwtProtectedHeaders, X509CertThumbprintHeader) validator.EXPECT().ValidateStrict(gomock.Any()) - resolve, documentMetadata, err := resolver.Resolve(rootDID, &metadata) + resolve, documentMetadata, err := didResolver.Resolve(rootDID, &metadata) require.NoError(t, err) assert.NotNil(t, resolve) assert.NotNil(t, documentMetadata) @@ -195,7 +197,7 @@ func TestManager_Resolve_OtherName(t *testing.T) { rootDID := did.MustParseDID(fmt.Sprintf("did:x509:0:%s:%s::san:otherName:%s", "sha384", sha384Sum(rootCertificate.Raw), otherNameValue)) delete(metadata.JwtProtectedHeaders, X509CertThumbprintHeader) validator.EXPECT().ValidateStrict(gomock.Any()) - resolve, documentMetadata, err := resolver.Resolve(rootDID, &metadata) + resolve, documentMetadata, err := didResolver.Resolve(rootDID, &metadata) require.NoError(t, err) assert.NotNil(t, resolve) assert.NotNil(t, documentMetadata) @@ -203,115 +205,111 @@ func TestManager_Resolve_OtherName(t *testing.T) { }) t.Run("broken thumbprint at x5t", func(t *testing.T) { metadata.JwtProtectedHeaders[X509CertThumbprintHeader] = "GARBAGE" - _, _, err := resolver.Resolve(rootDID, &metadata) + _, _, err := didResolver.Resolve(rootDID, &metadata) require.Error(t, err) assert.ErrorIs(t, ErrCertificateNotfound, err) metadata.JwtProtectedHeaders[X509CertThumbprintHeader] = sha1Sum(signingCert.Raw) }) t.Run("broken thumbprint at x5t#S256", func(t *testing.T) { metadata.JwtProtectedHeaders[X509CertThumbprintS256Header] = "GARBAGE" - _, _, err := resolver.Resolve(rootDID, &metadata) + _, _, err := didResolver.Resolve(rootDID, &metadata) require.Error(t, err) assert.ErrorIs(t, ErrCertificateNotfound, err) metadata.JwtProtectedHeaders[X509CertThumbprintS256Header] = sha256Sum(signingCert.Raw) }) t.Run("broken thumbprint with wrong hash at x5t", func(t *testing.T) { metadata.JwtProtectedHeaders[X509CertThumbprintHeader] = sha1Sum(rootCertificate.Raw) - _, _, err := resolver.Resolve(rootDID, &metadata) + _, _, err := didResolver.Resolve(rootDID, &metadata) require.Error(t, err) assert.ErrorIs(t, err, ErrNoMatchingHeaderCredentials) metadata.JwtProtectedHeaders[X509CertThumbprintHeader] = sha1Sum(signingCert.Raw) }) t.Run("broken thumbprint with wrong hash at x5t#S256", func(t *testing.T) { metadata.JwtProtectedHeaders[X509CertThumbprintS256Header] = sha256Sum(rootCertificate.Raw) - _, _, err := resolver.Resolve(rootDID, &metadata) + _, _, err := didResolver.Resolve(rootDID, &metadata) require.Error(t, err) assert.ErrorIs(t, err, ErrNoMatchingHeaderCredentials) metadata.JwtProtectedHeaders[X509CertThumbprintS256Header] = sha256Sum(signingCert.Raw) }) t.Run("invalid signature of root certificate", func(t *testing.T) { t.Skip("Can't test this right now, enable after https://github.com/nuts-foundation/nuts-node/issues/3587 has been fixed") - _, _, rootCert, _, _, err := BuildCertChain([]string{otherNameValue, otherNameValueSecondary}) + craftedCerts, _, err := testpki.BuildCertChain([]string{otherNameValue, otherNameValueSecondary}, "") require.NoError(t, err) craftedCertChain := new(cert.Chain) - require.NoError(t, craftedCertChain.Add(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: rootCert.Raw}))) + require.NoError(t, craftedCertChain.Add([]byte(base64.StdEncoding.EncodeToString(rootCertFromCerts(craftedCerts).Raw)))) // Do not add first cert, since it's the root CA cert, which should be the crafted certificate - for i := 1; i < certChain.Len(); i++ { - curr, b := certChain.Get(i) - require.True(t, b) - require.NoError(t, craftedCertChain.Add(curr)) + for i := 1; i < len(certs); i++ { + require.NoError(t, craftedCertChain.Add([]byte(base64.StdEncoding.EncodeToString(certs[i].Raw)))) } - metadata := resolver2.ResolveMetadata{} + metadata := resolver.ResolveMetadata{} metadata.JwtProtectedHeaders = make(map[string]interface{}) metadata.JwtProtectedHeaders[X509CertChainHeader] = craftedCertChain - _, _, err = resolver.Resolve(rootDID, &metadata) + _, _, err = didResolver.Resolve(rootDID, &metadata) require.ErrorContains(t, err, "did:509 certificate chain validation failed: x509: certificate signed by unknown authority") }) - t.Run("invalid signature of leaf certificate", func(t *testing.T) { - _, _, _, _, craftedSigningCert, err := BuildCertChain([]string{otherNameValue, otherNameValueSecondary}) + t.Run("invalid issuer signature of leaf certificate", func(t *testing.T) { + craftedCerts, _, err := testpki.BuildCertChain([]string{otherNameValue, otherNameValueSecondary}, "") require.NoError(t, err) craftedCertChain := new(cert.Chain) - // Do not add last cert, since it's the leaf, which should be the crafted certificate - for i := 0; i < certChain.Len()-1; i++ { - curr, b := certChain.Get(i) - require.True(t, b) - require.NoError(t, craftedCertChain.Add(curr)) + // Do not add first cert, since it's the leaf, which should be the crafted certificate + require.NoError(t, craftedCertChain.Add([]byte(base64.StdEncoding.EncodeToString(leafCertFromCerts(craftedCerts).Raw)))) + for i := 1; i < len(certs); i++ { + require.NoError(t, craftedCertChain.Add([]byte(base64.StdEncoding.EncodeToString(certs[i].Raw)))) } - require.NoError(t, craftedCertChain.Add(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: craftedSigningCert.Raw}))) - metadata := resolver2.ResolveMetadata{} + metadata := resolver.ResolveMetadata{} metadata.JwtProtectedHeaders = make(map[string]interface{}) metadata.JwtProtectedHeaders[X509CertChainHeader] = craftedCertChain - metadata.JwtProtectedHeaders[X509CertThumbprintHeader] = sha1Sum(craftedSigningCert.Raw) + metadata.JwtProtectedHeaders[X509CertThumbprintHeader] = sha1Sum(leafCertFromCerts(craftedCerts).Raw) - _, _, err = resolver.Resolve(rootDID, &metadata) + _, _, err = didResolver.Resolve(rootDID, &metadata) require.ErrorContains(t, err, "did:509 certificate chain validation failed: x509: certificate signed by unknown authority") }) t.Run("broken chain", func(t *testing.T) { expectedErr := errors.New("broken chain") validator.EXPECT().ValidateStrict(gomock.Any()).Return(expectedErr) - _, _, err := resolver.Resolve(rootDID, &metadata) + _, _, err := didResolver.Resolve(rootDID, &metadata) require.Error(t, err) assert.ErrorIs(t, err, expectedErr) }) t.Run("wrong otherName value", func(t *testing.T) { rootDID := did.MustParseDID(fmt.Sprintf("did:x509:0:%s:%s::san:otherName:%s", "sha256", sha256Sum(rootCertificate.Raw), "ANOTHER_BIG_STRING")) - _, _, err := resolver.Resolve(rootDID, &metadata) + _, _, err := didResolver.Resolve(rootDID, &metadata) require.Error(t, err) assert.EqualError(t, err, "the SAN attribute otherName does not match the query") }) t.Run("wrong hash type value", func(t *testing.T) { rootDID := did.MustParseDID(fmt.Sprintf("did:x509:0:%s:%s::san:otherName:%s", "test", sha256Sum(rootCertificate.Raw), otherNameValue)) - _, _, err := resolver.Resolve(rootDID, &metadata) + _, _, err := didResolver.Resolve(rootDID, &metadata) require.Error(t, err) assert.ErrorIs(t, ErrUnsupportedHashAlgorithm, err) }) t.Run("wrong hash value", func(t *testing.T) { rootDID := did.MustParseDID(fmt.Sprintf("did:x509:0:%s:%s::san:otherName:%s", "sha256", "test", otherNameValue)) - _, _, err := resolver.Resolve(rootDID, &metadata) + _, _, err := didResolver.Resolve(rootDID, &metadata) require.Error(t, err) assert.ErrorIs(t, ErrCertificateNotfound, err) }) t.Run("wrong DID type", func(t *testing.T) { expectedErr := fmt.Sprintf("unsupported DID method: %s", "test") rootDID := did.MustParseDID("did:test:example.com:testing") - _, _, err := resolver.Resolve(rootDID, &metadata) + _, _, err := didResolver.Resolve(rootDID, &metadata) require.Error(t, err) assert.EqualError(t, err, expectedErr) }) t.Run("wrong x509 did version", func(t *testing.T) { rootDID := did.MustParseDID(fmt.Sprintf("did:x509:1:%s:%s::san:otherName:%s", "sha256", sha256Sum(rootCertificate.Raw), "ANOTHER_BIG_STRING")) - _, _, err := resolver.Resolve(rootDID, &metadata) + _, _, err := didResolver.Resolve(rootDID, &metadata) require.Error(t, err) assert.ErrorIs(t, err, ErrDidVersion) }) t.Run("missing x509 hash unk", func(t *testing.T) { rootDID := did.MustParseDID(fmt.Sprintf("did:x509:%s:%s::san:otherName:%s", "unk", sha256Sum(rootCertificate.Raw), "ANOTHER_BIG_STRING")) - _, _, err := resolver.Resolve(rootDID, &metadata) + _, _, err := didResolver.Resolve(rootDID, &metadata) require.Error(t, err) assert.ErrorIs(t, err, ErrDidMalformed) }) @@ -320,32 +318,33 @@ func TestManager_Resolve_OtherName(t *testing.T) { func TestManager_Resolve_San_Generic(t *testing.T) { ctrl := gomock.NewController(t) validator := pki.NewMockValidator(ctrl) - resolver := NewResolver(validator) - metadata := resolver2.ResolveMetadata{} + didResolver := NewResolver(validator) + metadata := resolver.ResolveMetadata{} - _, certChain, rootCertificate, _, signingCert, err := BuildCertChain([]string{}) + certs, _, err := testpki.BuildCertChain([]string{}, "") require.NoError(t, err) + rootCertificate := rootCertFromCerts(certs) metadata.JwtProtectedHeaders = make(map[string]interface{}) - metadata.JwtProtectedHeaders[X509CertChainHeader] = certChain - metadata.JwtProtectedHeaders[X509CertThumbprintHeader] = sha1Sum(signingCert.Raw) - metadata.JwtProtectedHeaders[X509CertThumbprintS256Header] = sha256Sum(signingCert.Raw) + metadata.JwtProtectedHeaders[X509CertChainHeader] = testpki.CertsToChain(certs) + metadata.JwtProtectedHeaders[X509CertThumbprintHeader] = sha1Sum(leafCertFromCerts(certs).Raw) + metadata.JwtProtectedHeaders[X509CertThumbprintS256Header] = sha256Sum(leafCertFromCerts(certs).Raw) t.Run("unk san attribute", func(t *testing.T) { rootDID := did.MustParseDID(fmt.Sprintf("did:x509:0:%s:%s::san:unknown:%s", "sha256", sha256Sum(rootCertificate.Raw), "www.uva.nl")) - _, _, err := resolver.Resolve(rootDID, &metadata) + _, _, err := didResolver.Resolve(rootDID, &metadata) require.Error(t, err) assert.EqualError(t, err, "unknown policy key: unknown for policy: san") }) t.Run("impartial san attribute", func(t *testing.T) { rootDID := did.MustParseDID(fmt.Sprintf("did:x509:0:%s:%s::san:%s", "sha256", sha256Sum(rootCertificate.Raw), "www.uva.nl")) - _, _, err := resolver.Resolve(rootDID, &metadata) + _, _, err := didResolver.Resolve(rootDID, &metadata) require.Error(t, err) assert.ErrorIs(t, err, ErrDidPolicyMalformed) }) t.Run("broken san attribute", func(t *testing.T) { rootDID := did.MustParseDID(fmt.Sprintf("did:x509:0:%s:%s::san:dns:%s", "sha256", sha256Sum(rootCertificate.Raw), "www.uva.nl")) rootDID.ID = strings.Replace(rootDID.ID, "www.uva.nl", "www.uva%2.nl", 1) - _, _, err := resolver.Resolve(rootDID, &metadata) + _, _, err := didResolver.Resolve(rootDID, &metadata) require.Error(t, err) assert.EqualError(t, err, "invalid URL escape \"%2.\"") }) @@ -353,14 +352,14 @@ func TestManager_Resolve_San_Generic(t *testing.T) { rootDID := did.MustParseDID(fmt.Sprintf("did:x509:0:%s:%s::san:dns:%s", "sha256", sha256Sum(rootCertificate.Raw), "www.example.com")) validator.EXPECT().ValidateStrict(gomock.Any()) - resolve, documentMetadata, err := resolver.Resolve(rootDID, &metadata) + resolve, documentMetadata, err := didResolver.Resolve(rootDID, &metadata) require.NoError(t, err) assert.NotNil(t, resolve) assert.NotNil(t, documentMetadata) }) t.Run("error SAN DNS", func(t *testing.T) { rootDID := did.MustParseDID(fmt.Sprintf("did:x509:0:%s:%s::san:dns:%s", "sha256", sha256Sum(rootCertificate.Raw), "www.uva.nl")) - _, _, err := resolver.Resolve(rootDID, &metadata) + _, _, err := didResolver.Resolve(rootDID, &metadata) require.Error(t, err) assert.EqualError(t, err, "the SAN attribute dns does not match the query") }) @@ -368,14 +367,14 @@ func TestManager_Resolve_San_Generic(t *testing.T) { rootDID := did.MustParseDID(fmt.Sprintf("did:x509:0:%s:%s::san:ip:%s", "sha256", sha256Sum(rootCertificate.Raw), "192.1.2.3")) validator.EXPECT().ValidateStrict(gomock.Any()) - resolve, documentMetadata, err := resolver.Resolve(rootDID, &metadata) + resolve, documentMetadata, err := didResolver.Resolve(rootDID, &metadata) require.NoError(t, err) assert.NotNil(t, resolve) assert.NotNil(t, documentMetadata) }) t.Run("error SAN ip", func(t *testing.T) { rootDID := did.MustParseDID(fmt.Sprintf("did:x509:0:%s:%s::san:ip:%s", "sha256", sha256Sum(rootCertificate.Raw), "10.0.0.1")) - _, _, err := resolver.Resolve(rootDID, &metadata) + _, _, err := didResolver.Resolve(rootDID, &metadata) require.Error(t, err) assert.EqualError(t, err, "the SAN attribute ip does not match the query") }) @@ -383,14 +382,14 @@ func TestManager_Resolve_San_Generic(t *testing.T) { rootDID := did.MustParseDID(fmt.Sprintf("did:x509:0:%s:%s::san:email:%s", "sha256", sha256Sum(rootCertificate.Raw), "info%40example.com")) validator.EXPECT().ValidateStrict(gomock.Any()) - resolve, documentMetadata, err := resolver.Resolve(rootDID, &metadata) + resolve, documentMetadata, err := didResolver.Resolve(rootDID, &metadata) require.NoError(t, err) assert.NotNil(t, resolve) assert.NotNil(t, documentMetadata) }) t.Run("error SAN email", func(t *testing.T) { rootDID := did.MustParseDID(fmt.Sprintf("did:x509:0:%s:%s::san:email:%s", "sha256", sha256Sum(rootCertificate.Raw), "bad%40example.com")) - _, _, err := resolver.Resolve(rootDID, &metadata) + _, _, err := didResolver.Resolve(rootDID, &metadata) require.Error(t, err) assert.EqualError(t, err, "the SAN attribute email does not match the query") }) @@ -399,27 +398,28 @@ func TestManager_Resolve_San_Generic(t *testing.T) { func TestManager_Resolve_Subject(t *testing.T) { ctrl := gomock.NewController(t) validator := pki.NewMockValidator(ctrl) - resolver := NewResolver(validator) - metadata := resolver2.ResolveMetadata{} + didResolver := NewResolver(validator) + metadata := resolver.ResolveMetadata{} otherNameValue := "A_BIG_STRING" - _, certChain, rootCertificate, _, signingCert, err := BuildCertChain([]string{otherNameValue}) + certs, _, err := testpki.BuildCertChain([]string{otherNameValue}, "") require.NoError(t, err) + rootCertificate := rootCertFromCerts(certs) metadata.JwtProtectedHeaders = make(map[string]interface{}) - metadata.JwtProtectedHeaders[X509CertChainHeader] = certChain - metadata.JwtProtectedHeaders[X509CertThumbprintHeader] = sha1Sum(signingCert.Raw) - metadata.JwtProtectedHeaders[X509CertThumbprintS256Header] = sha256Sum(signingCert.Raw) + metadata.JwtProtectedHeaders[X509CertChainHeader] = testpki.CertsToChain(certs) + metadata.JwtProtectedHeaders[X509CertThumbprintHeader] = sha1Sum(leafCertFromCerts(certs).Raw) + metadata.JwtProtectedHeaders[X509CertThumbprintS256Header] = sha256Sum(leafCertFromCerts(certs).Raw) t.Run("unknown policy", func(t *testing.T) { rootDID := did.MustParseDID(fmt.Sprintf("did:x509:0:%s:%s::unknown:CN:%s", "sha256", sha256Sum(rootCertificate.Raw), "www.nuts.nl")) - _, _, err := resolver.Resolve(rootDID, &metadata) + _, _, err := didResolver.Resolve(rootDID, &metadata) require.Error(t, err) assert.ErrorIs(t, err, ErrUnkPolicyType) }) t.Run("unknown policy key", func(t *testing.T) { rootDID := did.MustParseDID(fmt.Sprintf("did:x509:0:%s:%s::subject:UNK:%s", "sha256", sha256Sum(rootCertificate.Raw), "www.nuts.nl")) - _, _, err := resolver.Resolve(rootDID, &metadata) + _, _, err := didResolver.Resolve(rootDID, &metadata) require.Error(t, err) assert.EqualError(t, err, "unknown policy key: UNK for policy: subject") @@ -427,14 +427,14 @@ func TestManager_Resolve_Subject(t *testing.T) { t.Run("broken subject attribute", func(t *testing.T) { rootDID := did.MustParseDID(fmt.Sprintf("did:x509:0:%s:%s::subject:CN:%s", "sha256", sha256Sum(rootCertificate.Raw), "www.nuts.nl")) rootDID.ID = strings.Replace(rootDID.ID, "www.nuts.nl", "www.nuts%2.nl", 1) - _, _, err := resolver.Resolve(rootDID, &metadata) + _, _, err := didResolver.Resolve(rootDID, &metadata) require.Error(t, err) assert.Equal(t, "invalid URL escape \"%2.\"", err.Error()) }) t.Run("impartial subject attribute", func(t *testing.T) { rootDID := did.MustParseDID(fmt.Sprintf("did:x509:0:%s:%s::subject:%s", "sha256", sha256Sum(rootCertificate.Raw), "www.nuts.nl")) - _, _, err := resolver.Resolve(rootDID, &metadata) + _, _, err := didResolver.Resolve(rootDID, &metadata) require.Error(t, err) assert.ErrorIs(t, err, ErrDidPolicyMalformed) @@ -442,21 +442,21 @@ func TestManager_Resolve_Subject(t *testing.T) { t.Run("happy flow CN www.example.com", func(t *testing.T) { rootDID := did.MustParseDID(fmt.Sprintf("did:x509:0:%s:%s::subject:CN:%s", "sha256", sha256Sum(rootCertificate.Raw), "www.example.com")) validator.EXPECT().ValidateStrict(gomock.Any()) - resolve, documentMetadata, err := resolver.Resolve(rootDID, &metadata) + resolve, documentMetadata, err := didResolver.Resolve(rootDID, &metadata) require.NoError(t, err) assert.NotNil(t, resolve) assert.NotNil(t, documentMetadata) }) t.Run("error flow CN bad.example.com", func(t *testing.T) { rootDID := did.MustParseDID(fmt.Sprintf("did:x509:0:%s:%s::subject:CN:%s", "sha256", sha256Sum(rootCertificate.Raw), "bad.example.com")) - _, _, err := resolver.Resolve(rootDID, &metadata) + _, _, err := didResolver.Resolve(rootDID, &metadata) require.Error(t, err) assert.Equal(t, "query does not match the subject : CN", err.Error()) }) t.Run("happy flow O", func(t *testing.T) { rootDID := did.MustParseDID(fmt.Sprintf("did:x509:0:%s:%s::subject:O:%s", "sha256", sha256Sum(rootCertificate.Raw), "NUTS%20Foundation")) validator.EXPECT().ValidateStrict(gomock.Any()) - resolve, documentMetadata, err := resolver.Resolve(rootDID, &metadata) + resolve, documentMetadata, err := didResolver.Resolve(rootDID, &metadata) require.NoError(t, err) assert.NotNil(t, resolve) assert.NotNil(t, documentMetadata) @@ -464,7 +464,7 @@ func TestManager_Resolve_Subject(t *testing.T) { t.Run("happy flow O and CN", func(t *testing.T) { rootDID := did.MustParseDID(fmt.Sprintf("did:x509:0:%s:%s::subject:O:%s::subject:CN:%s", "sha256", sha256Sum(rootCertificate.Raw), "NUTS%20Foundation", "www.example.com")) validator.EXPECT().ValidateStrict(gomock.Any()) - resolve, documentMetadata, err := resolver.Resolve(rootDID, &metadata) + resolve, documentMetadata, err := didResolver.Resolve(rootDID, &metadata) require.NoError(t, err) assert.NotNil(t, resolve) assert.NotNil(t, documentMetadata) @@ -472,45 +472,45 @@ func TestManager_Resolve_Subject(t *testing.T) { t.Run("happy flow O and CN and OU", func(t *testing.T) { rootDID := did.MustParseDID(fmt.Sprintf("did:x509:0:%s:%s::subject:O:%s::subject:CN:%s::subject:OU:%s", "sha256", sha256Sum(rootCertificate.Raw), "NUTS%20Foundation", "www.example.com", "The%20A-Team")) validator.EXPECT().ValidateStrict(gomock.Any()) - resolve, documentMetadata, err := resolver.Resolve(rootDID, &metadata) + resolve, documentMetadata, err := didResolver.Resolve(rootDID, &metadata) require.NoError(t, err) assert.NotNil(t, resolve) assert.NotNil(t, documentMetadata) }) t.Run("error flow O and CN broken policy", func(t *testing.T) { rootDID := did.MustParseDID(fmt.Sprintf("did:x509:0:%s:%s::subject:O:%s::subject:CV:%s", "sha256", sha256Sum(rootCertificate.Raw), "NUTS%20Foundation", "www.example.com")) - _, _, err := resolver.Resolve(rootDID, &metadata) + _, _, err := didResolver.Resolve(rootDID, &metadata) require.Error(t, err) assert.Equal(t, "unknown policy key: CV for policy: subject", err.Error()) }) t.Run("error flow O and CN broken policy: extra :", func(t *testing.T) { rootDID := did.MustParseDID(fmt.Sprintf("did:x509:0:%s:%s::subject:O:%s::subject:CN:%s:", "sha256", sha256Sum(rootCertificate.Raw), "NUTS%20Foundation", "www.example.com")) - _, _, err := resolver.Resolve(rootDID, &metadata) + _, _, err := didResolver.Resolve(rootDID, &metadata) require.Error(t, err) assert.ErrorIs(t, err, ErrDidPolicyMalformed) }) t.Run("error flow O and CN broken policy, extra :: ", func(t *testing.T) { rootDID := did.MustParseDID(fmt.Sprintf("did:x509:0:%s:%s::subject:O:%s::subject:CN:%s::", "sha256", sha256Sum(rootCertificate.Raw), "NUTS%20Foundation", "www.example.com")) - _, _, err := resolver.Resolve(rootDID, &metadata) + _, _, err := didResolver.Resolve(rootDID, &metadata) require.Error(t, err) assert.ErrorIs(t, err, ErrDidPolicyMalformed) }) t.Run("error flow O and CN broken policy, extra : and garbage ", func(t *testing.T) { rootDID := did.MustParseDID(fmt.Sprintf("did:x509:0:%s:%s::subject:O:%s::subject:CN:%s:test:", "sha256", sha256Sum(rootCertificate.Raw), "NUTS%20Foundation", "www.example.com")) - _, _, err := resolver.Resolve(rootDID, &metadata) + _, _, err := didResolver.Resolve(rootDID, &metadata) require.Error(t, err) assert.Equal(t, "unknown policy key: test for policy: subject", err.Error()) }) t.Run("error flow O", func(t *testing.T) { rootDID := did.MustParseDID(fmt.Sprintf("did:x509:0:%s:%s::subject:O:%s", "sha256", sha256Sum(rootCertificate.Raw), "UNKNOW%20NUTS%20Foundation")) - _, _, err := resolver.Resolve(rootDID, &metadata) + _, _, err := didResolver.Resolve(rootDID, &metadata) require.Error(t, err) assert.EqualError(t, err, "query does not match the subject : O") }) t.Run("happy flow L Amsterdam", func(t *testing.T) { rootDID := did.MustParseDID(fmt.Sprintf("did:x509:0:%s:%s::subject:L:%s", "sha256", sha256Sum(rootCertificate.Raw), "Amsterdam")) validator.EXPECT().ValidateStrict(gomock.Any()) - resolve, documentMetadata, err := resolver.Resolve(rootDID, &metadata) + resolve, documentMetadata, err := didResolver.Resolve(rootDID, &metadata) require.NoError(t, err) assert.NotNil(t, resolve) assert.NotNil(t, documentMetadata) @@ -518,56 +518,56 @@ func TestManager_Resolve_Subject(t *testing.T) { t.Run("happy flow L Den Haag", func(t *testing.T) { rootDID := did.MustParseDID(fmt.Sprintf("did:x509:0:%s:%s::subject:L:%s", "sha256", sha256Sum(rootCertificate.Raw), "The%20Hague")) validator.EXPECT().ValidateStrict(gomock.Any()) - resolve, documentMetadata, err := resolver.Resolve(rootDID, &metadata) + resolve, documentMetadata, err := didResolver.Resolve(rootDID, &metadata) require.NoError(t, err) assert.NotNil(t, resolve) assert.NotNil(t, documentMetadata) }) t.Run("error flow L", func(t *testing.T) { rootDID := did.MustParseDID(fmt.Sprintf("did:x509:0:%s:%s::subject:L:%s", "sha256", sha256Sum(rootCertificate.Raw), "Rotterdam")) - _, _, err := resolver.Resolve(rootDID, &metadata) + _, _, err := didResolver.Resolve(rootDID, &metadata) require.Error(t, err) assert.EqualError(t, err, "query does not match the subject : L") }) t.Run("happy flow C", func(t *testing.T) { rootDID := did.MustParseDID(fmt.Sprintf("did:x509:0:%s:%s::subject:C:%s", "sha256", sha256Sum(rootCertificate.Raw), "NL")) validator.EXPECT().ValidateStrict(gomock.Any()) - resolve, documentMetadata, err := resolver.Resolve(rootDID, &metadata) + resolve, documentMetadata, err := didResolver.Resolve(rootDID, &metadata) require.NoError(t, err) assert.NotNil(t, resolve) assert.NotNil(t, documentMetadata) }) t.Run("error flow C", func(t *testing.T) { rootDID := did.MustParseDID(fmt.Sprintf("did:x509:0:%s:%s::subject:C:%s", "sha256", sha256Sum(rootCertificate.Raw), "BE")) - _, _, err := resolver.Resolve(rootDID, &metadata) + _, _, err := didResolver.Resolve(rootDID, &metadata) require.Error(t, err) assert.EqualError(t, err, "query does not match the subject : C") }) t.Run("happy flow ST", func(t *testing.T) { rootDID := did.MustParseDID(fmt.Sprintf("did:x509:0:%s:%s::subject:ST:%s", "sha256", sha256Sum(rootCertificate.Raw), "Noord-Holland")) validator.EXPECT().ValidateStrict(gomock.Any()) - resolve, documentMetadata, err := resolver.Resolve(rootDID, &metadata) + resolve, documentMetadata, err := didResolver.Resolve(rootDID, &metadata) require.NoError(t, err) assert.NotNil(t, resolve) assert.NotNil(t, documentMetadata) }) t.Run("error flow ST ", func(t *testing.T) { rootDID := did.MustParseDID(fmt.Sprintf("did:x509:0:%s:%s::subject:ST:%s", "sha256", sha256Sum(rootCertificate.Raw), "Noord-Brabant")) - _, _, err := resolver.Resolve(rootDID, &metadata) + _, _, err := didResolver.Resolve(rootDID, &metadata) require.Error(t, err) assert.EqualError(t, err, "query does not match the subject : ST") }) t.Run("happy flow STREET", func(t *testing.T) { rootDID := did.MustParseDID(fmt.Sprintf("did:x509:0:%s:%s::subject:STREET:%s", "sha256", sha256Sum(rootCertificate.Raw), "Amsterdamseweg%20100")) validator.EXPECT().ValidateStrict(gomock.Any()) - resolve, documentMetadata, err := resolver.Resolve(rootDID, &metadata) + resolve, documentMetadata, err := didResolver.Resolve(rootDID, &metadata) require.NoError(t, err) assert.NotNil(t, resolve) assert.NotNil(t, documentMetadata) }) t.Run("error flow STREET", func(t *testing.T) { rootDID := did.MustParseDID(fmt.Sprintf("did:x509:0:%s:%s::subject:STREET:%s", "sha256", sha256Sum(rootCertificate.Raw), "Haarlemsetraatweg%2099")) - _, _, err := resolver.Resolve(rootDID, &metadata) + _, _, err := didResolver.Resolve(rootDID, &metadata) require.Error(t, err) assert.EqualError(t, err, "query does not match the subject : STREET") }) @@ -575,28 +575,28 @@ func TestManager_Resolve_Subject(t *testing.T) { t.Run("happy flow serialNumber", func(t *testing.T) { rootDID := did.MustParseDID(fmt.Sprintf("did:x509:0:%s:%s::subject:serialNumber:%s", "sha256", sha256Sum(rootCertificate.Raw), "32121323")) validator.EXPECT().ValidateStrict(gomock.Any()) - resolve, documentMetadata, err := resolver.Resolve(rootDID, &metadata) + resolve, documentMetadata, err := didResolver.Resolve(rootDID, &metadata) require.NoError(t, err) assert.NotNil(t, resolve) assert.NotNil(t, documentMetadata) }) t.Run("error flow serialNumber", func(t *testing.T) { rootDID := did.MustParseDID(fmt.Sprintf("did:x509:0:%s:%s::subject:serialNumber:%s", "sha256", sha256Sum(rootCertificate.Raw), "1")) - _, _, err := resolver.Resolve(rootDID, &metadata) + _, _, err := didResolver.Resolve(rootDID, &metadata) require.Error(t, err) assert.EqualError(t, err, "query does not match the subject : serialNumber") }) t.Run("happy flow OU", func(t *testing.T) { rootDID := did.MustParseDID(fmt.Sprintf("did:x509:0:%s:%s::subject:OU:%s", "sha256", sha256Sum(rootCertificate.Raw), "The%20A-Team")) validator.EXPECT().ValidateStrict(gomock.Any()) - resolve, documentMetadata, err := resolver.Resolve(rootDID, &metadata) + resolve, documentMetadata, err := didResolver.Resolve(rootDID, &metadata) require.NoError(t, err) assert.NotNil(t, resolve) assert.NotNil(t, documentMetadata) }) t.Run("error flow OU", func(t *testing.T) { rootDID := did.MustParseDID(fmt.Sprintf("did:x509:0:%s:%s::subject:OU:%s", "sha256", sha256Sum(rootCertificate.Raw), "The%20B-Team")) - _, _, err := resolver.Resolve(rootDID, &metadata) + _, _, err := didResolver.Resolve(rootDID, &metadata) require.Error(t, err) assert.EqualError(t, err, "query does not match the subject : OU") }) diff --git a/vdr/didx509/x509_utils.go b/vdr/didx509/x509_utils.go index 8912c8826e..b4be0cacd6 100644 --- a/vdr/didx509/x509_utils.go +++ b/vdr/didx509/x509_utils.go @@ -27,7 +27,6 @@ import ( "crypto/x509/pkix" "encoding/asn1" "encoding/base64" - "encoding/pem" "errors" "fmt" "github.com/lestrrat-go/jwx/v2/cert" @@ -62,9 +61,6 @@ var ( // ErrCertificateNotfound indicates that a certificate could not be found with the given hash. ErrCertificateNotfound = fmt.Errorf("cannot find a certificate with the given hash") - // ErrInvalidPemBlock indicates that a PEM block is invalid or cannot be decoded properly. - ErrInvalidPemBlock = fmt.Errorf("invalid PEM block") - // ErrTrailingData indicates that there is trailing data after an X.509 extension, which should not be present. ErrTrailingData = errors.New("x509: trailing data after X.509 extension") @@ -177,21 +173,16 @@ func parseChain(headerChain *cert.Chain) ([]*x509.Certificate, error) { } chain := make([]*x509.Certificate, headerChain.Len()) for i := range headerChain.Len() { - certBytes, has := headerChain.Get(i) - if has { - pemBlock, _ := pem.Decode(certBytes) - if pemBlock == nil { - return nil, ErrInvalidPemBlock - } - if pemBlock.Type != "CERTIFICATE" { - return nil, fmt.Errorf("invalid PEM block type: %s", pemBlock.Type) - } - certificate, err := x509.ParseCertificate(pemBlock.Bytes) - if err != nil { - return nil, err - } - chain[i] = certificate + certBytes, _ := headerChain.Get(i) + certRawBytes, err := base64.StdEncoding.DecodeString(string(certBytes)) + if err != nil { + return nil, err + } + certificate, err := x509.ParseCertificate(certRawBytes) + if err != nil { + return nil, err } + chain[i] = certificate } return chain, nil } diff --git a/vdr/didx509/x509_utils_test.go b/vdr/didx509/x509_utils_test.go index 911a09b2fa..9f0a9b1421 100644 --- a/vdr/didx509/x509_utils_test.go +++ b/vdr/didx509/x509_utils_test.go @@ -20,258 +20,25 @@ package didx509 import ( "bytes" - "crypto/rand" - "crypto/rsa" "crypto/x509" "crypto/x509/pkix" "encoding/asn1" "encoding/base64" - "encoding/pem" "errors" "fmt" "github.com/lestrrat-go/jwx/v2/cert" - "math/big" - "net" + "github.com/nuts-foundation/nuts-node/test/pki" + "github.com/stretchr/testify/require" "slices" "strings" "testing" - "time" ) -// BuildCertChain generates a certificate chain, including root, intermediate, and signing certificates. -func BuildCertChain(identifiers []string) (chainCerts [4]*x509.Certificate, chain *cert.Chain, rootCertificate *x509.Certificate, signingKey *rsa.PrivateKey, signingCert *x509.Certificate, err error) { - chainCerts = [4]*x509.Certificate{} - chain = &cert.Chain{} - rootKey, rootCert, rootPem, err := buildRootCert() - if err != nil { - return chainCerts, nil, nil, nil, nil, err - } - chainCerts[0] = rootCert - err = chain.Add(rootPem) - if err != nil { - return chainCerts, nil, nil, nil, nil, err - } - - intermediateL1Key, intermediateL1Cert, intermediateL1Pem, err := buildIntermediateCert(rootCert, rootKey, "Intermediate CA Level 1") - if err != nil { - return chainCerts, nil, nil, nil, nil, err - } - chainCerts[1] = intermediateL1Cert - err = chain.Add(intermediateL1Pem) - if err != nil { - return chainCerts, nil, nil, nil, nil, err - } - - intermediateL2Key, intermediateL2Cert, intermediateL2Pem, err := buildIntermediateCert(intermediateL1Cert, intermediateL1Key, "Intermediate CA Level 2") - chainCerts[2] = intermediateL2Cert - err = chain.Add(intermediateL2Pem) - if err != nil { - return chainCerts, nil, nil, nil, nil, err - } - - signingKey, signingCert, signingPEM, err := buildSigningCert(identifiers, intermediateL2Cert, intermediateL2Key, "32121323") - if err != nil { - return chainCerts, nil, nil, nil, nil, err - } - chainCerts[3] = signingCert - err = chain.Add(signingPEM) - if err != nil { - return chainCerts, nil, nil, nil, nil, err - } - return chainCerts, chain, rootCert, signingKey, signingCert, nil -} - -func buildSigningCert(identifiers []string, intermediateL2Cert *x509.Certificate, intermediateL2Key *rsa.PrivateKey, serialNumber string) (*rsa.PrivateKey, *x509.Certificate, []byte, error) { - signingKey, err := rsa.GenerateKey(rand.Reader, 2048) - if err != nil { - return nil, nil, nil, err - } - signingTmpl, err := SigningCertTemplate(nil, identifiers) - if err != nil { - return nil, nil, nil, err - } - signingTmpl.Subject.SerialNumber = serialNumber - signingCert, signingPEM, err := CreateCert(signingTmpl, intermediateL2Cert, &signingKey.PublicKey, intermediateL2Key) - if err != nil { - return nil, nil, nil, err - } - return signingKey, signingCert, signingPEM, err -} - -func buildIntermediateCert(parentCert *x509.Certificate, parentKey *rsa.PrivateKey, subjectName string) (*rsa.PrivateKey, *x509.Certificate, []byte, error) { - intermediateL1Key, err := rsa.GenerateKey(rand.Reader, 2048) - if err != nil { - return nil, nil, nil, err - } - intermediateL1Tmpl, err := CertTemplate(subjectName) - if err != nil { - return nil, nil, nil, err - } - intermediateL1Cert, intermediateL1Pem, err := CreateCert(intermediateL1Tmpl, parentCert, &intermediateL1Key.PublicKey, parentKey) - if err != nil { - return nil, nil, nil, err - } - return intermediateL1Key, intermediateL1Cert, intermediateL1Pem, nil -} - -func buildRootCert() (*rsa.PrivateKey, *x509.Certificate, []byte, error) { - rootKey, err := rsa.GenerateKey(rand.Reader, 2048) - if err != nil { - return nil, nil, nil, err - } - rootCertTmpl, err := CertTemplate("Root CA") - if err != nil { - return nil, nil, nil, err - } - rootCert, rootPem, err := CreateCert(rootCertTmpl, rootCertTmpl, &rootKey.PublicKey, rootKey) - if err != nil { - return nil, nil, nil, err - } - return rootKey, rootCert, rootPem, nil -} - -// CertTemplate generates a template for a x509 certificate with a given serial number. If no serial number is provided, a random one is generated. -// The certificate is valid for one month and uses SHA256 with RSA for the signature algorithm. -func CertTemplate(subjectName string) (*x509.Certificate, error) { - serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 8) - serialNumber, _ := rand.Int(rand.Reader, serialNumberLimit) - tmpl := x509.Certificate{ - SerialNumber: serialNumber, - Subject: pkix.Name{Organization: []string{subjectName}}, - SignatureAlgorithm: x509.SHA256WithRSA, - NotBefore: time.Now(), - NotAfter: time.Now().Add(time.Hour * 24 * 30), // valid for a month - BasicConstraintsValid: true, - } - tmpl.IsCA = true - tmpl.KeyUsage = x509.KeyUsageCertSign | x509.KeyUsageDigitalSignature - tmpl.ExtKeyUsage = []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth} - return &tmpl, nil -} - -// SigningCertTemplate creates a x509.Certificate template for a signing certificate with an optional serial number. -func SigningCertTemplate(serialNumber *big.Int, identifiers []string) (*x509.Certificate, error) { - // generate a random serial number (a real cert authority would have some logic behind this) - if serialNumber == nil { - serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 8) - serialNumber, _ = rand.Int(rand.Reader, serialNumberLimit) - } - - tmpl := x509.Certificate{ - SignatureAlgorithm: x509.SHA256WithRSA, - SerialNumber: serialNumber, - Subject: pkix.Name{ - Organization: []string{"NUTS Foundation"}, - CommonName: "www.example.com", - Country: []string{"NL"}, - Locality: []string{"Amsterdam", "The Hague"}, - OrganizationalUnit: []string{"The A-Team"}, - StreetAddress: []string{"Amsterdamseweg 100"}, - PostalCode: []string{"1011 NL"}, - Province: []string{"Noord-Holland"}, - }, - NotBefore: time.Now(), - NotAfter: time.Now().Add(time.Hour * 24 * 30), // valid for a month - } - tmpl.KeyUsage = x509.KeyUsageDigitalSignature - tmpl.ExtKeyUsage = []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth} - // Either the ExtraExtensions SubjectAlternativeNameType is set, or the Subject Alternate Name values are set, - // both don't mix - if len(identifiers) > 0 { - err := setSanAlternativeName(&tmpl, identifiers, "testhost.example.com") - if err != nil { - return nil, err - } - } else { - tmpl.DNSNames = []string{"www.example.com", "example.com"} - tmpl.EmailAddresses = []string{"info@example.com", "no-reply@example.org"} - tmpl.IPAddresses = []net.IP{net.ParseIP("192.1.2.3"), net.ParseIP("192.1.2.4")} - } - return &tmpl, nil -} - -func setSanAlternativeName(tmpl *x509.Certificate, identifiers []string, altHostName string) error { - var list []asn1.RawValue - // Add the alternative host name first - value, err := toRawValue(altHostName, "tag:2") - if err != nil { - return err - } - list = append(list, *value) - - for _, identifier := range identifiers { - raw, err := toRawValue(identifier, "ia5") - if err != nil { - return err - } - otherName := OtherName{ - TypeID: OtherNameType, - Value: asn1.RawValue{ - Class: 2, - Tag: 0, - IsCompound: true, - Bytes: raw.FullBytes, - }, - } - - raw, err = toRawValue(otherName, "tag:0") - if err != nil { - return err - } - list = append(list, *raw) - } - marshal, err := asn1.Marshal(list) - if err != nil { - return err - } - tmpl.ExtraExtensions = append(tmpl.ExtraExtensions, pkix.Extension{ - Id: SubjectAlternativeNameType, - Critical: false, - Value: marshal, - }) - return nil -} - -// toRawValue marshals an ASN.1 identifier with a given tag, then unmarshals it into a RawValue structure. -func toRawValue(value any, tag string) (*asn1.RawValue, error) { - b, err := asn1.MarshalWithParams(value, tag) - if err != nil { - return nil, err - } - var val asn1.RawValue - _, err = asn1.Unmarshal(b, &val) - if err != nil { - return nil, err - } - return &val, nil -} - -// CreateCert generates a new x509 certificate using the provided template and parent certificates, public and private keys. -// It returns the generated certificate, its PEM-encoded version, and any error encountered during the process. -func CreateCert(template, parent *x509.Certificate, pub interface{}, parentPriv interface{}) (cert *x509.Certificate, certPEM []byte, err error) { - - certDER, err := x509.CreateCertificate(rand.Reader, template, parent, pub, parentPriv) - if err != nil { - return nil, nil, err - } - // parse the resulting certificate so we can use it again - cert, err = x509.ParseCertificate(certDER) - if err != nil { - return nil, nil, err - } - // PEM encode the certificate (this is a standard TLS encoding) - b := pem.Block{Type: "CERTIFICATE", Bytes: certDER} - certPEM = pem.EncodeToMemory(&b) - return cert, certPEM, err -} - func TestFindOtherNameValue(t *testing.T) { - t.Parallel() - key, certificate, _, err := buildRootCert() - _, signingCert, _, err := buildSigningCert([]string{"123", "321"}, certificate, key, "4567") - if err != nil { - t.Fatalf("failed to build root certificate: %v", err) - } + key, certificate, err := pki.BuildRootCert() + require.NoError(t, err) + _, signingCert, err := pki.BuildSigningCert([]string{"123", "321"}, certificate, key, "4567") + require.NoError(t, err) tests := []struct { name string @@ -321,11 +88,8 @@ func TestFindCertificateByHash(t *testing.T) { } return base64.RawURLEncoding.EncodeToString(h) } - chainCerts, _, _, _, _, err := BuildCertChain([]string{"123"}) - if err != nil { - t.Error(err) - } - t.Parallel() + chainCerts, _, err := pki.BuildCertChain([]string{"123"}, "") + require.NoError(t, err) type testCase struct { name string chain []*x509.Certificate @@ -422,17 +186,10 @@ func TestParseChain(t *testing.T) { } return &chain } - certs, chain, _, _, _, _ := BuildCertChain([]string{"123"}) + certs, _, _ := pki.BuildCertChain([]string{"123"}, "") - invalidPEM := `-----BEGIN CERTIFICATE----- -Y29ycnVwdCBjZXJ0aWZpY2F0ZQo= ------END CERTIFICATE-----` - emptyTypePEM := `-----BEGIN CIPHER TEXT----- -MIIEDTCCAvegAwIBAgIQAf2j627KdciIQ4tyS8+8kTANBgkqhkiG9w0BAQsFADA/ ------END CIPHER TEXT-----` - invalidBase64PEM := `-----BEGIN CERTIFICATE----- -Hello, world! ------END CERTIFICATE-----` + invalidCert := `Y29ycnVwdCBjZXJ0aWZpY2F0ZQo=` + invalidBase64 := `Hello, world!` tests := []struct { name string chain *cert.Chain @@ -446,35 +203,29 @@ Hello, world! wantError: nil, }, { name: "valid certificate", - chain: chain, + chain: pki.CertsToChain(certs), want: certs[:], // not critical for testing wantError: nil, }, { - name: "invalid PEM", - chain: newChain([][]byte{[]byte(invalidPEM)}), + name: "invalid cert", + chain: newChain([][]byte{[]byte(invalidCert)}), want: nil, wantError: errors.New("x509: malformed certificate"), }, { - name: "PEM with empty type", - chain: newChain([][]byte{[]byte(emptyTypePEM)}), - want: nil, - wantError: fmt.Errorf("invalid PEM block type: %s", "CIPHER TEXT"), - }, { - name: "invalid base64 in PEM", - chain: newChain([][]byte{[]byte(invalidBase64PEM)}), + name: "invalid base64", + chain: newChain([][]byte{[]byte(invalidBase64)}), want: nil, - wantError: ErrInvalidPemBlock, + wantError: errors.New("illegal base64 data at input byte 5"), }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { chain, foundErr := parseChain(tt.chain) - if !errors.Is(foundErr, tt.wantError) { - if foundErr.Error() != tt.wantError.Error() { - t.Errorf("parseChain() error = %v, want: %v", foundErr, tt.wantError) - } - return + if tt.wantError == nil { + require.NoError(t, foundErr) + } else { + require.EqualError(t, foundErr, tt.wantError.Error()) } if len(chain) != len(tt.want) { t.Errorf("parseChain() error, wrong number of parsed certs: %d, want: %d", len(chain), len(tt.want)) @@ -484,6 +235,14 @@ Hello, world! } } +func leafCertFromCerts(certs []*x509.Certificate) *x509.Certificate { + return certs[0] +} + +func rootCertFromCerts(certs []*x509.Certificate) *x509.Certificate { + return certs[len(certs)-1] +} + func TestProcessSANSequence(t *testing.T) { asn1Marshal := func(data interface{}) []byte { marshal, _ := asn1.Marshal(data)