From fa2ee19f65d58314101d8f8f86745ac3fd1b4cfc Mon Sep 17 00:00:00 2001 From: Bas Westerbaan Date: Fri, 6 Jun 2025 13:19:43 +0200 Subject: [PATCH] x509: Add VerifyOptions.UnknownAlgorithmVerifier This allows callers to verify certificates using algorithms that Go does not support (yet). For instance, here we're verifying the ML-KEM-512 example certificate from the LAMPS WG signed by a ML-DSA-44 public key. https://github.com/lamps-wg/dilithium-certificates/blob/main/examples/ML-DSA-44.crt https://github.com/lamps-wg/kyber-certificates/blob/main/example/ML-KEM-512.crt package main import ( "crypto/x509" "crypto/x509/pkix" "encoding/asn1" "encoding/pem" "errors" "fmt" "github.com/cloudflare/circl/sign/schemes" "os" ) func loadCert(path string) (*x509.Certificate, error) { raw, err := os.ReadFile(path) if err != nil { return nil, fmt.Errorf("ReadFile(%s): %w", path, err) } block, _ := pem.Decode(raw) if block == nil { return nil, fmt.Errorf("pem.Decode(%s) failed", path) } return x509.ParseCertificate(block.Bytes) } func main() { dsaCert, err := loadCert("ML-DSA-44.crt") if err != nil { panic(err) } kemCert, err := loadCert("ML-KEM-512.crt") if err != nil { panic(err) } roots := x509.NewCertPool() roots.AddCert(dsaCert) _, err = kemCert.Verify(x509.VerifyOptions{ Roots: roots, UnknownAlgorithmVerifier: func(alg pkix.AlgorithmIdentifier, signed, signature, pk []byte) error { if !alg.Algorithm.Equal(asn1.ObjectIdentifier{2, 16, 840, 1, 101, 3, 4, 3, 17}) { return errors.New("unsupported scheme") } scheme := schemes.ByName("ML-DSA-44") ppk, err := scheme.UnmarshalBinaryPublicKey(pk) if err != nil { return err } if !scheme.Verify(ppk, signed, signature, nil) { return errors.New("invalid signature") } return nil }, }) if err != nil { panic(err) } } --- src/crypto/x509/parser.go | 13 ++++++---- src/crypto/x509/root_windows.go | 4 +-- src/crypto/x509/verify.go | 6 ++++- src/crypto/x509/x509.go | 43 ++++++++++++++++++++++++++------- 4 files changed, 49 insertions(+), 17 deletions(-) diff --git a/src/crypto/x509/parser.go b/src/crypto/x509/parser.go index 4abcc1b7b590e2..c0560ad1f7f687 100644 --- a/src/crypto/x509/parser.go +++ b/src/crypto/x509/parser.go @@ -1038,11 +1038,14 @@ func parseCertificate(der []byte) (*Certificate, error) { if !spki.ReadASN1BitString(&spk) { return nil, errors.New("x509: malformed subjectPublicKey") } - if cert.PublicKeyAlgorithm != UnknownPublicKeyAlgorithm { - cert.PublicKey, err = parsePublicKey(&publicKeyInfo{ - Algorithm: pkAI, - PublicKey: spk, - }) + pki := &publicKeyInfo{ + Algorithm: pkAI, + PublicKey: spk, + } + if cert.PublicKeyAlgorithm == UnknownPublicKeyAlgorithm { + cert.PublicKey = pki + } else { + cert.PublicKey, err = parsePublicKey(pki) if err != nil { return nil, err } diff --git a/src/crypto/x509/root_windows.go b/src/crypto/x509/root_windows.go index 4bea1081618a65..2b88dfb7507810 100644 --- a/src/crypto/x509/root_windows.go +++ b/src/crypto/x509/root_windows.go @@ -190,8 +190,8 @@ func verifyChain(c *Certificate, chainCtx *syscall.CertChainContext, opts *Verif if parent.PublicKeyAlgorithm != ECDSA { continue } - if err := parent.CheckSignature(chain[i].SignatureAlgorithm, - chain[i].RawTBSCertificate, chain[i].Signature); err != nil { + if err := checkSignature(chain[i].SignatureAlgorithm, + chain[i].RawTBSCertificate, chain[i].Signature, parent.PublicKey, true, opts); err != nil { return nil, err } } diff --git a/src/crypto/x509/verify.go b/src/crypto/x509/verify.go index 7cc0fb2e3e0385..65ade74391cdfb 100644 --- a/src/crypto/x509/verify.go +++ b/src/crypto/x509/verify.go @@ -218,6 +218,10 @@ type VerifyOptions struct { // field implies any valid policy is acceptable. CertificatePolicies []OID + // UnknownAlgorithmVerifier specifies a callback to use to verify + // a signature with an unknown AlgorithmIdentifier. + UnknownAlgorithmVerifier func(alg pkix.AlgorithmIdentifier, signed, signature, pk []byte) error + // The following policy fields are unexported, because we do not expect // users to actually need to use them, but are useful for testing the // policy validation code. @@ -975,7 +979,7 @@ func (c *Certificate) buildChains(currentChain []*Certificate, sigChecks *int, o return } - if err := c.CheckSignatureFrom(candidate.cert); err != nil { + if err := c.checkSignatureFrom(candidate.cert, opts); err != nil { if hintErr == nil { hintErr = err hintCert = candidate.cert diff --git a/src/crypto/x509/x509.go b/src/crypto/x509/x509.go index 1f06b4fbc578fe..626d657b892b68 100644 --- a/src/crypto/x509/x509.go +++ b/src/crypto/x509/x509.go @@ -205,6 +205,17 @@ type publicKeyInfo struct { PublicKey asn1.BitString } +func (pki *publicKeyInfo) Equal(other crypto.PublicKey) bool { + pki2, ok := other.(*publicKeyInfo) + if !ok { + return false + } + return (pki.Algorithm.Algorithm.Equal(pki2.Algorithm.Algorithm) && + bytes.Equal(pki.Algorithm.Parameters.FullBytes, pki2.Algorithm.Parameters.FullBytes) && + pki.PublicKey.BitLength == pki2.PublicKey.BitLength && + bytes.Equal(pki.PublicKey.Bytes, pki2.PublicKey.Bytes)) +} + // RFC 5280, 4.2.1.1 type authKeyId struct { Id []byte `asn1:"optional,tag:0"` @@ -909,6 +920,10 @@ func (c *Certificate) hasSANExtension() bool { // This is a low-level API that performs very limited checks, and not a full // path verifier. Most users should use [Certificate.Verify] instead. func (c *Certificate) CheckSignatureFrom(parent *Certificate) error { + return c.checkSignatureFrom(parent, nil) +} + +func (c *Certificate) checkSignatureFrom(parent *Certificate, opts *VerifyOptions) error { // RFC 5280, 4.2.1.9: // "If the basic constraints extension is not present in a version 3 // certificate, or the extension is present but the cA boolean is not @@ -923,11 +938,7 @@ func (c *Certificate) CheckSignatureFrom(parent *Certificate) error { return ConstraintViolationError{} } - if parent.PublicKeyAlgorithm == UnknownPublicKeyAlgorithm { - return ErrUnsupportedAlgorithm - } - - return checkSignature(c.SignatureAlgorithm, c.RawTBSCertificate, c.Signature, parent.PublicKey, false) + return checkSignature(c.SignatureAlgorithm, c.RawTBSCertificate, c.Signature, parent.PublicKey, false, opts) } // CheckSignature verifies that signature is a valid signature over signed from @@ -938,7 +949,7 @@ func (c *Certificate) CheckSignatureFrom(parent *Certificate) error { // [MD5WithRSA] signatures are rejected, while [SHA1WithRSA] and [ECDSAWithSHA1] // signatures are currently accepted. func (c *Certificate) CheckSignature(algo SignatureAlgorithm, signed, signature []byte) error { - return checkSignature(algo, signed, signature, c.PublicKey, true) + return checkSignature(algo, signed, signature, c.PublicKey, true, nil) } func (c *Certificate) hasNameConstraints() bool { @@ -960,10 +971,24 @@ func signaturePublicKeyAlgoMismatchError(expectedPubKeyAlgo PublicKeyAlgorithm, // checkSignature verifies that signature is a valid signature over signed from // a crypto.PublicKey. -func checkSignature(algo SignatureAlgorithm, signed, signature []byte, publicKey crypto.PublicKey, allowSHA1 bool) (err error) { +func checkSignature(algo SignatureAlgorithm, signed, signature []byte, publicKey crypto.PublicKey, allowSHA1 bool, opts *VerifyOptions) (err error) { var hashType crypto.Hash var pubKeyAlgo PublicKeyAlgorithm + if algo == UnknownSignatureAlgorithm { + pki, ok := publicKey.(*publicKeyInfo) + if !ok || opts == nil || opts.UnknownAlgorithmVerifier == nil { + return ErrUnsupportedAlgorithm + } + + return opts.UnknownAlgorithmVerifier( + pki.Algorithm, + signed, + signature, + pki.PublicKey.Bytes, + ) + } + for _, details := range signatureAlgorithmDetails { if details.algo == algo { hashType = details.hash @@ -1585,7 +1610,7 @@ func signTBS(tbs []byte, key crypto.Signer, sigAlg SignatureAlgorithm, rand io.R } // Check the signature to ensure the crypto.Signer behaved correctly. - if err := checkSignature(sigAlg, tbs, signature, key.Public(), true); err != nil { + if err := checkSignature(sigAlg, tbs, signature, key.Public(), true, nil); err != nil { return nil, fmt.Errorf("x509: signature returned by signer is invalid: %w", err) } @@ -2259,7 +2284,7 @@ func parseCertificateRequest(in *certificateRequest) (*CertificateRequest, error // CheckSignature reports whether the signature on c is valid. func (c *CertificateRequest) CheckSignature() error { - return checkSignature(c.SignatureAlgorithm, c.RawTBSCertificateRequest, c.Signature, c.PublicKey, true) + return checkSignature(c.SignatureAlgorithm, c.RawTBSCertificateRequest, c.Signature, c.PublicKey, true, nil) } // RevocationListEntry represents an entry in the revokedCertificates