Skip to content

Commit

Permalink
#3587: validate certificate chain when resolving did:x509 DIDs (#3589)
Browse files Browse the repository at this point in the history
  • Loading branch information
reinkrul authored Dec 10, 2024
1 parent 478229c commit 942c46e
Show file tree
Hide file tree
Showing 7 changed files with 133 additions and 61 deletions.
25 changes: 15 additions & 10 deletions core/tls.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,13 +87,23 @@ func LoadTrustStore(trustStoreFile string) (*TrustStore, error) {

// ParseTrustStore creates a x509 certificate pool from the raw data
func ParseTrustStore(data []byte) (*TrustStore, error) {
var err error
trustStore := new(TrustStore)

trustStore.certificates, err = ParseCertificates(data)
certificates, err := ParseCertificates(data)
if err != nil {
return nil, err
}
trustStore := BuildTrustStore(certificates)

if err = validate(trustStore); err != nil {
return nil, err
}

return trustStore, nil
}

// BuildTrustStore creates a TrustStore from the given certificates, separating them into root and intermediate CAs
func BuildTrustStore(certificates []*x509.Certificate) *TrustStore {
trustStore := new(TrustStore)
trustStore.certificates = certificates
trustStore.CertPool = NewCertPool(trustStore.certificates)

for _, certificate := range trustStore.certificates {
Expand All @@ -106,12 +116,7 @@ func ParseTrustStore(data []byte) (*TrustStore, error) {
}
}
}

if err = validate(trustStore); err != nil {
return nil, err
}

return trustStore, nil
return trustStore
}

// validate returns an error if one of the certificates is invalid or does not form a chain to some root
Expand Down
19 changes: 19 additions & 0 deletions core/tls_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"github.com/nuts-foundation/nuts-node/test/pki"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"os"
"testing"
)

Expand Down Expand Up @@ -54,3 +55,21 @@ func TestLoadTrustStore(t *testing.T) {
assert.EqualError(t, err, "x509: certificate signed by unknown authority")
})
}

func TestBuildTrustStore(t *testing.T) {
t.Run("ok", func(t *testing.T) {
caBundleData, err := os.ReadFile("../pki/test/pkioverheid-server-bundle.pem")
require.NoError(t, err)
certs, err := ParseCertificates(caBundleData)
require.NoError(t, err)

store := BuildTrustStore(certs)

// Assert root certs
require.Len(t, store.RootCAs, 1)
assert.Equal(t, "CN=Staat der Nederlanden EV Root CA,O=Staat der Nederlanden,C=NL", store.RootCAs[0].Subject.String())
// Assert intermediate certs
require.Len(t, store.IntermediateCAs, 2)
assert.Equal(t, "CN=Staat der Nederlanden Domein Server CA 2020,O=Staat der Nederlanden,C=NL", store.IntermediateCAs[1].Subject.String())
})
}
63 changes: 26 additions & 37 deletions vcr/verifier/signature_verifier_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -344,13 +344,11 @@ func buildX509Credential(chainPems *cert.Chain, signingCert *x509.Certificate, r
}

func buildCertChain(ura string) (*cert.Chain, *x509.Certificate, *rsa.PrivateKey, *x509.Certificate, error) {
chain := [4]*x509.Certificate{}
chainPems := &cert.Chain{}
rootKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return nil, nil, nil, nil, err
}
rootCertTmpl, err := CertTemplate(nil)
rootCertTmpl, err := CertTemplate("Root CA")
if err != nil {
return nil, nil, nil, nil, err
}
Expand All @@ -361,71 +359,65 @@ func buildCertChain(ura string) (*cert.Chain, *x509.Certificate, *rsa.PrivateKey
if err != nil {
return nil, nil, nil, nil, err
}
chain[3] = rootCert
err = chainPems.Add(rootPem)
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(nil)
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
}
chain[2] = intermediateL1Cert
err = chainPems.Add(intermediateL1Pem)
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(nil)
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
}
chain[1] = intermediateL2Cert
err = chainPems.Add(intermediateL2Pem)

signingKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return nil, nil, nil, nil, err
}

signingKey, err := rsa.GenerateKey(rand.Reader, 2048)
signingTmpl, err := CertTemplate("Leaf")
if err != nil {
return nil, nil, nil, nil, err
}
signingTmpl, err := CertTemplate(nil)
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
}
chain[0] = signingCert
err = chainPems.Add(signingPEM)
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
}
}
chainPems, err = fixChainHeaders(chainPems)
return chainPems, rootCert, signingKey, signingCert, nil

return certChain, rootCert, signingKey, signingCert, nil
}

func testUraCredential(did string, ura string) (*vc.VerifiableCredential, error) {
Expand Down Expand Up @@ -480,15 +472,13 @@ func x509VerifierTestSetup(t testing.TB) (signatureVerifier, *pki.MockValidator)
}

// CertTemplate is a helper function to create a cert template with a serial number and other required fields
func CertTemplate(serialNumber *big.Int) (*x509.Certificate, error) {
func CertTemplate(subjectName 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)
}
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{"JaegerTracing"}},
Subject: pkix.Name{Organization: []string{subjectName}},
SignatureAlgorithm: x509.SHA256WithRSA,
NotBefore: time.Now(),
NotAfter: time.Now().Add(time.Hour * 24 * 30), // valid for a month
Expand All @@ -498,19 +488,18 @@ func CertTemplate(serialNumber *big.Int) (*x509.Certificate, error) {
}

// 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 []byte, err error) {

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, nil, err
return nil, "", err
}
// parse the resulting certificate so we can use it again
cert, err = x509.ParseCertificate(certDER)
if err != nil {
return nil, nil, err
return nil, "", err
}
// PEM encode the certificate (this is a standard TLS encoding)
b := pem.Block{Type: "CERTIFICATE", Bytes: certDER}
certPEM = pem.EncodeToMemory(&b)
certPEM = string(pem.EncodeToMemory(&b))
return cert, certPEM, err
}
20 changes: 19 additions & 1 deletion vdr/didx509/resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
"fmt"
ssi "github.com/nuts-foundation/go-did"
"github.com/nuts-foundation/go-did/did"
"github.com/nuts-foundation/nuts-node/core"
"github.com/nuts-foundation/nuts-node/pki"
"github.com/nuts-foundation/nuts-node/vdr/resolver"
"strings"
Expand Down Expand Up @@ -115,12 +116,29 @@ func (r Resolver) Resolve(id did.DID, metadata *resolver.ResolveMetadata) (*did.
return nil, nil, err
}

// Validate certificate chain, checking signatures and whether the chain is complete
var chainWithoutLeaf []*x509.Certificate
for _, curr := range chain {
if curr.Equal(validationCert) {
continue
}
chainWithoutLeaf = append(chainWithoutLeaf, curr)
}
trustStore := core.BuildTrustStore(chainWithoutLeaf)
verifiedChains, err := validationCert.Verify(x509.VerifyOptions{
Intermediates: core.NewCertPool(trustStore.IntermediateCAs),
Roots: core.NewCertPool(trustStore.RootCAs),
})
if err != nil {
return nil, nil, fmt.Errorf("did:509 certificate chain validation failed: %w", err)
}

err = validatePolicy(ref, validationCert)
if err != nil {
return nil, nil, err
}

err = r.pkiValidator.CheckCRLStrict(chain)
err = r.pkiValidator.CheckCRLStrict(verifiedChains[0])
if err != nil {
return nil, nil, err
}
Expand Down
44 changes: 44 additions & 0 deletions vdr/didx509/resolver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,10 @@ 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"
Expand Down Expand Up @@ -227,6 +229,48 @@ func TestManager_Resolve_OtherName(t *testing.T) {
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})
require.NoError(t, err)

craftedCertChain := new(cert.Chain)
require.NoError(t, craftedCertChain.Add(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: rootCert.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))
}

metadata := resolver2.ResolveMetadata{}
metadata.JwtProtectedHeaders = make(map[string]interface{})
metadata.JwtProtectedHeaders[X509CertChainHeader] = craftedCertChain

_, _, err = resolver.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})
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))
}
require.NoError(t, craftedCertChain.Add(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: craftedSigningCert.Raw})))

metadata := resolver2.ResolveMetadata{}
metadata.JwtProtectedHeaders = make(map[string]interface{})
metadata.JwtProtectedHeaders[X509CertChainHeader] = craftedCertChain
metadata.JwtProtectedHeaders[X509CertThumbprintHeader] = sha1Sum(craftedSigningCert.Raw)

_, _, err = resolver.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)
Expand Down
2 changes: 1 addition & 1 deletion vdr/didx509/x509_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ var (
ErrInvalidHash = fmt.Errorf("invalid hash")

// ErrCertificateNotfound indicates that a certificate could not be found with the given hash.
ErrCertificateNotfound = fmt.Errorf("cannot locate a find a certificate 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")
Expand Down
21 changes: 9 additions & 12 deletions vdr/didx509/x509_utils_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ func BuildCertChain(identifiers []string) (chainCerts [4]*x509.Certificate, chai
return chainCerts, nil, nil, nil, nil, err
}

intermediateL1Key, intermediateL1Cert, intermediateL1Pem, err := buildIntermediateCert(err, rootCert, rootKey)
intermediateL1Key, intermediateL1Cert, intermediateL1Pem, err := buildIntermediateCert(rootCert, rootKey, "Intermediate CA Level 1")
if err != nil {
return chainCerts, nil, nil, nil, nil, err
}
Expand All @@ -62,7 +62,7 @@ func BuildCertChain(identifiers []string) (chainCerts [4]*x509.Certificate, chai
return chainCerts, nil, nil, nil, nil, err
}

intermediateL2Key, intermediateL2Cert, intermediateL2Pem, err := buildIntermediateCert(err, intermediateL1Cert, intermediateL1Key)
intermediateL2Key, intermediateL2Cert, intermediateL2Pem, err := buildIntermediateCert(intermediateL1Cert, intermediateL1Key, "Intermediate CA Level 2")
chainCerts[2] = intermediateL2Cert
err = chain.Add(intermediateL2Pem)
if err != nil {
Expand Down Expand Up @@ -98,12 +98,12 @@ func buildSigningCert(identifiers []string, intermediateL2Cert *x509.Certificate
return signingKey, signingCert, signingPEM, err
}

func buildIntermediateCert(err error, parentCert *x509.Certificate, parentKey *rsa.PrivateKey) (*rsa.PrivateKey, *x509.Certificate, []byte, error) {
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(nil)
intermediateL1Tmpl, err := CertTemplate(subjectName)
if err != nil {
return nil, nil, nil, err
}
Expand All @@ -119,7 +119,7 @@ func buildRootCert() (*rsa.PrivateKey, *x509.Certificate, []byte, error) {
if err != nil {
return nil, nil, nil, err
}
rootCertTmpl, err := CertTemplate(nil)
rootCertTmpl, err := CertTemplate("Root CA")
if err != nil {
return nil, nil, nil, err
}
Expand All @@ -132,15 +132,12 @@ func buildRootCert() (*rsa.PrivateKey, *x509.Certificate, []byte, error) {

// 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(serialNumber *big.Int) (*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)
}
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{"JaegerTracing"}},
Subject: pkix.Name{Organization: []string{subjectName}},
SignatureAlgorithm: x509.SHA256WithRSA,
NotBefore: time.Now(),
NotAfter: time.Now().Add(time.Hour * 24 * 30), // valid for a month
Expand Down

0 comments on commit 942c46e

Please sign in to comment.