Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,9 @@ mocks/

# install location for project-local tools such as golangci-lint
tools-bin/

# Private keys. Test CA keys (rootCA.key, intermediateCA.key) are generated
# locally by scripts/gen-certs.sh and must never be committed.
# endEntity.key is tracked because it is go:embed-ed in testdata/testdata.go.
*.key
!testdata/endEntity.key
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,18 @@ make presubmit
```
and check its output to make sure your code coverage figures are in line with the set target and that there are no newly introduced lint problems.

## x5chain trust verification

Signed CoRIM messages may carry an X.509 chain in the COSE `x5chain` protected header.
Use [`SignedCorim.VerifyWithX5Chain`](https://pkg.go.dev/github.com/veraison/corim/corim#SignedCorim.VerifyWithX5Chain)
after [`FromCOSE`](https://pkg.go.dev/github.com/veraison/corim/corim#SignedCorim.FromCOSE) to validate PKIX trust, optional CRL revocation, and the COSE signature.

Load trust material with [`LoadTrustAnchors`](https://pkg.go.dev/github.com/veraison/corim/corim#LoadTrustAnchors).
When no trust-anchor paths are supplied, verification uses the OS certificate store; for production deployments, pass explicit anchors.
When no CRL paths are supplied, revocation checks are skipped; when CRLs are loaded, [`CrlPolicyStrict`](https://pkg.go.dev/github.com/veraison/corim/corim#CrlPolicyStrict) is the default.

For external-key verification without PKIX path validation, use [`SignedCorim.Verify`](https://pkg.go.dev/github.com/veraison/corim/corim#SignedCorim.Verify) instead.

## Extending CoRIM/CoMID

The CoRIM specification provides a mechanism for adding extensions to the base
Expand Down
97 changes: 97 additions & 0 deletions corim/example_x5chain_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
// Copyright 2021-2026 Contributors to the Veraison project.
// SPDX-License-Identifier: Apache-2.0

package corim

import (
"fmt"
"log"
"time"

"github.com/veraison/corim/testdata"
)

func x5chainExampleMeta() *Meta {
notAfter := time.Date(2021, time.October, 0, 0, 0, 0, 0, time.UTC)

return NewMeta().
SetSigner("ACME Ltd.", nil).
SetValidity(notAfter, nil)
}

func exampleSignedCorimWithX5Chain() ([]byte, error) {
signer, err := NewSignerFromJWK(testEndEntityKey)
if err != nil {
return nil, err
}

var unsigned UnsignedCorim
if err := unsigned.FromCBOR(testGoodUnsignedCorimCBOR); err != nil {
return nil, err
}

var signed SignedCorim
signed.UnsignedCorim = unsigned
signed.Meta = *x5chainExampleMeta()

if err := signed.AddSigningCert(testdata.EndEntityDer); err != nil {
return nil, err
}

intermediates := make([]byte, len(testdata.IntermediateCA)+len(testdata.RootCA))
copy(intermediates, testdata.IntermediateCA)
copy(intermediates[len(testdata.IntermediateCA):], testdata.RootCA)

if err := signed.AddIntermediateCerts(intermediates); err != nil {
return nil, err
}

return signed.Sign(signer)
}

func ExampleSignedCorim_VerifyWithX5Chain() {
cbor, err := exampleSignedCorimWithX5Chain()
if err != nil {
log.Fatal(err)
}

anchors, err := LoadTrustAnchors(func(path string) ([]byte, error) {
if path != "anchor.der" {
return nil, fmt.Errorf("unknown path %q", path)
}

return testdata.RootCA, nil
}, []string{"anchor.der"}, nil)
if err != nil {
log.Fatal(err)
}

var signed SignedCorim
if err := signed.FromCOSE(cbor); err != nil {
log.Fatal(err)
}

if err := signed.VerifyWithX5Chain(anchors); err != nil {
log.Fatal(err)
}
// Output:
}

func ExampleLoadTrustAnchors() {
anchors, err := LoadTrustAnchors(func(path string) ([]byte, error) {
switch path {
case "anchor.der":
return testdata.RootCA, nil
default:
return nil, fmt.Errorf("unknown path %q", path)
}
}, []string{"anchor.der"}, nil)
if err != nil {
log.Fatal(err)
}

if anchors.Pool == nil {
log.Fatal("expected explicit trust-anchor pool")
}
// Output:
}
166 changes: 142 additions & 24 deletions corim/signedcorim.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"errors"
"fmt"
"strings"
"time"

"github.com/veraison/corim/extensions"
cose "github.com/veraison/go-cose"
Expand All @@ -20,6 +21,8 @@ var (
ContentType = "application/rim+cbor"
NoExternalData = []byte("")
HeaderLabelCorimMeta = int64(8)

errNoSign1Message = errors.New("no Sign1 message found")
)

// SignedCorim encodes a signed-corim message (i.e., a COSE Sign1 wrapped CoRIM)
Expand Down Expand Up @@ -117,68 +120,139 @@ func (o *SignedCorim) extractMeta(v interface{}) error {
}

func (o *SignedCorim) extractX5Chain(x5chain interface{}) error {
var buf bytes.Buffer
var (
signingCert *x509.Certificate
intermediateCerts []*x509.Certificate
err error
)

switch t := x5chain.(type) {
case []interface{}:
elems := make([][]byte, len(t))
for i, elem := range t {
cert, ok := elem.([]byte)
certDER, ok := elem.([]byte)
if !ok {
return fmt.Errorf("accessing x5chain[%d]: got %T, want []byte", i, elem)
}

switch i {
case 0:
if err := o.AddSigningCert(cert); err != nil {
return fmt.Errorf("decoding x5chain: %w", err)
}
default:
buf.Write(cert)
}
elems[i] = certDER
}

if buf.Len() > 0 {
if err := o.AddIntermediateCerts(buf.Bytes()); err != nil {
return fmt.Errorf("decoding x5chain: %w", err)
}
}
signingCert, intermediateCerts, err = parseX5ChainFromCertDERs(elems)
case [][]byte:
signingCert, intermediateCerts, err = parseX5ChainFromCertDERs(t)
case []byte:
if err := o.AddSigningCert(t); err != nil {
return fmt.Errorf("decoding x5chain: %w", err)
}
signingCert, err = parseX5ChainLeafDER(t)
default:
return fmt.Errorf("decoding x5chain: got %T, want []interface{} or []byte", t)
return fmt.Errorf("decoding x5chain: got %T, want []interface{}, [][]byte, or []byte", t)
}

if err != nil {
return err
}

o.SigningCert = signingCert
o.IntermediateCerts = intermediateCerts

return nil
}

func parseX5ChainFromCertDERs(elems [][]byte) (leaf *x509.Certificate, intermediates []*x509.Certificate, err error) {
if len(elems) == 0 {
return nil, nil, fmt.Errorf("decoding x5chain: empty certificate array")
}

leaf, err = parseX5ChainLeafDER(elems[0])
if err != nil {
return nil, nil, err
}

intermediates = make([]*x509.Certificate, 0, len(elems)-1)
for i := 1; i < len(elems); i++ {
var parsed *x509.Certificate
parsed, err = parseX5ChainIntermediateDER(elems[i], i)
if err != nil {
return nil, nil, err
}

intermediates = append(intermediates, parsed)
}

return leaf, intermediates, nil
}

func parseX5ChainLeafDER(der []byte) (*x509.Certificate, error) {
if der == nil {
return nil, fmt.Errorf("decoding x5chain: nil signing cert")
}
if len(der) == 0 {
return nil, fmt.Errorf("decoding x5chain: empty signing cert")
}

parsed, err := x509.ParseCertificate(der)
if err != nil {
return nil, fmt.Errorf("decoding x5chain: invalid signing certificate: %w", err)
}

return parsed, nil
}

func parseX5ChainIntermediateDER(der []byte, index int) (*x509.Certificate, error) {
if len(der) == 0 {
return nil, fmt.Errorf("decoding x5chain: empty intermediate cert at index %d", index)
}

certs, err := x509.ParseCertificates(der)
if err != nil {
return nil, fmt.Errorf("decoding x5chain: invalid intermediate certificate at index %d: %w", index, err)
}

if len(certs) != 1 {
return nil, fmt.Errorf("decoding x5chain: expected 1 certificate at index %d, got %d", index, len(certs))
}

return certs[0], nil
}

// FromCOSE decodes and effects syntactic validation on the supplied
// signed-corim message, including the embedded unsigned-corim and corim-meta.
// On success, the unsigned-corim-map is made available via the UnsignedCorim
// field while the corim-meta-map is decoded into the Meta field.
func (o *SignedCorim) FromCOSE(buf []byte) error {
o.message = cose.NewSign1Message()
o.SigningCert = nil
o.IntermediateCerts = nil

var err error
// Roll back partial decode on any failure. Later steps must assign to err (not :=)
// or this cleanup is skipped.
defer func() {
if err != nil {
o.message = nil
o.SigningCert = nil
o.IntermediateCerts = nil
}
}()

// If a tagged-corim-type-choice #6.500 of tagged-signed-corim #6.502, strip the prefix.
// This is a remnant of an older draft of the specification before
// https://github.com/ietf-rats-wg/draft-ietf-rats-corim/pull/337
corimTypeChoice := []byte("\xd9\x01\xf4\xd9\x01\xf6")
buf, _ = bytes.CutPrefix(buf, corimTypeChoice)

if err := o.message.UnmarshalCBOR(buf); err != nil {
if err = o.message.UnmarshalCBOR(buf); err != nil {
return fmt.Errorf("failed CBOR decoding for COSE-Sign1 signed CoRIM: %w", err)
}

if err := o.processHdrs(); err != nil {
if err = o.processHdrs(); err != nil {
return fmt.Errorf("processing COSE headers: %w", err)
}

if err := o.UnsignedCorim.FromCBOR(o.message.Payload); err != nil {
if err = o.UnsignedCorim.FromCBOR(o.message.Payload); err != nil {
return fmt.Errorf("failed CBOR decoding of unsigned CoRIM: %w", err)
}

if err := o.UnsignedCorim.Valid(); err != nil {
if err = o.UnsignedCorim.Valid(); err != nil {
return fmt.Errorf("failed validation of unsigned CoRIM: %w", err)
}

Expand Down Expand Up @@ -294,7 +368,7 @@ func (o *SignedCorim) Sign(signer cose.Signer) ([]byte, error) {
// supplied public key
func (o *SignedCorim) Verify(pk crypto.PublicKey) error {
if o.message == nil {
return errors.New("no Sign1 message found")
return errNoSign1Message
}

protected := o.message.Headers.Protected
Expand All @@ -316,3 +390,47 @@ func (o *SignedCorim) Verify(pk crypto.PublicKey) error {

return nil
}

// VerifyWithX5Chain validates the embedded x5chain and CoRIM COSE signature.
// Call [SignedCorim.FromCOSE] first. For external-key verify without PKIX, use [SignedCorim.Verify].
// Load trust material via [LoadTrustAnchors] when reading anchors/CRLs from files.
//
// Leaf policy rejects CA certificates. keyUsage is optional; when present,
// digitalSignature is required. PKIX validation uses ExtKeyUsageAny.
func (o *SignedCorim) VerifyWithX5Chain(anchors TrustAnchors) error {
if o.message == nil {
return errNoSign1Message
}

if o.SigningCert == nil {
return errors.New("x5chain: header not set in CoRIM")
}

chain := make([]*x509.Certificate, 0, 1+len(o.IntermediateCerts))
chain = append(chain, o.SigningCert)
chain = append(chain, o.IntermediateCerts...)

now := anchors.CurrentTime
if now.IsZero() {
now = time.Now()
}

if err := validateLeafSigningCert(o.SigningCert); err != nil {
return err
}

verifiedChain, err := verifyPKIXChain(chain, anchors, now)
if err != nil {
return err
}

if err := checkChainRevocation(verifiedChain, anchors.CRLs, anchors.CrlPolicy, now); err != nil {
return err
}

if err := o.Verify(verifiedChain[0].PublicKey); err != nil {
return fmt.Errorf("x5chain: COSE signature verification failed: %w", err)
}

return nil
}
Loading
Loading