From 8c458e278da806b56444d1cbfd6ec24fc28aad9c Mon Sep 17 00:00:00 2001 From: Steve Lasker Date: Mon, 15 Jul 2024 09:46:54 -0700 Subject: [PATCH] Add support for CWT Claims & Type in Protected Headers (#189) Signed-off-by: steve lasker Co-authored-by: Orie Steele --- cwt.go | 20 +++++++++++++ cwt_test.go | 82 +++++++++++++++++++++++++++++++++++++++++++++++++++++ headers.go | 57 +++++++++++++++++++++++++++++++++---- 3 files changed, 153 insertions(+), 6 deletions(-) create mode 100644 cwt.go create mode 100644 cwt_test.go diff --git a/cwt.go b/cwt.go new file mode 100644 index 0000000..e2aa9a2 --- /dev/null +++ b/cwt.go @@ -0,0 +1,20 @@ +package cose + +// https://www.iana.org/assignments/cwt/cwt.xhtml#claims-registry +const ( + CWTClaimIssuer int64 = 1 + CWTClaimSubject int64 = 2 + CWTClaimAudience int64 = 3 + CWTClaimExpirationTime int64 = 4 + CWTClaimNotBefore int64 = 5 + CWTClaimIssuedAt int64 = 6 + CWTClaimCWTID int64 = 7 + CWTClaimConfirmation int64 = 8 + CWTClaimScope int64 = 9 + + // TODO: the rest upon request +) + +// CWTClaims contains parameters that are to be cryptographically +// protected. +type CWTClaims map[any]any diff --git a/cwt_test.go b/cwt_test.go new file mode 100644 index 0000000..93dd0ae --- /dev/null +++ b/cwt_test.go @@ -0,0 +1,82 @@ +package cose_test + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "fmt" + + "github.com/veraison/go-cose" +) + +// This example demonstrates signing and verifying COSE_Sign1 signatures. +func ExampleCWTMessage() { + // create message to be signed + msgToSign := cose.NewSign1Message() + msgToSign.Payload = []byte("hello world") + msgToSign.Headers.Protected.SetAlgorithm(cose.AlgorithmES512) + + msgToSign.Headers.Protected.SetType("application/cwt") + claims := cose.CWTClaims{ + cose.CWTClaimIssuer: "issuer.example", + cose.CWTClaimSubject: "subject.example", + } + msgToSign.Headers.Protected.SetCWTClaims(claims) + + msgToSign.Headers.Unprotected[cose.HeaderLabelKeyID] = []byte("1") + + // create a signer + privateKey, err := ecdsa.GenerateKey(elliptic.P521(), rand.Reader) + if err != nil { + panic(err) + } + signer, err := cose.NewSigner(cose.AlgorithmES512, privateKey) + if err != nil { + panic(err) + } + + // sign message + err = msgToSign.Sign(rand.Reader, nil, signer) + if err != nil { + panic(err) + } + sig, err := msgToSign.MarshalCBOR() + // uncomment to review EDN + // coseSign1Diagnostic, err := cbor.Diagnose(sig) + // fmt.Println(coseSign1Diagnostic) + if err != nil { + panic(err) + } + fmt.Println("message signed") + + // create a verifier from a trusted public key + publicKey := privateKey.Public() + verifier, err := cose.NewVerifier(cose.AlgorithmES512, publicKey) + if err != nil { + panic(err) + } + + // verify message + var msgToVerify cose.Sign1Message + err = msgToVerify.UnmarshalCBOR(sig) + if err != nil { + panic(err) + } + err = msgToVerify.Verify(nil, verifier) + if err != nil { + panic(err) + } + fmt.Println("message verified") + + // tamper the message and verification should fail + msgToVerify.Payload = []byte("foobar") + err = msgToVerify.Verify(nil, verifier) + if err != cose.ErrVerification { + panic(err) + } + fmt.Println("verification error as expected") + // Output: + // message signed + // message verified + // verification error as expected +} diff --git a/headers.go b/headers.go index 2999207..19c2b3f 100644 --- a/headers.go +++ b/headers.go @@ -23,6 +23,8 @@ const ( HeaderLabelCounterSignature0 int64 = 9 HeaderLabelCounterSignatureV2 int64 = 11 HeaderLabelCounterSignature0V2 int64 = 12 + HeaderLabelCWTClaims int64 = 15 + HeaderLabelType int64 = 16 HeaderLabelX5Bag int64 = 32 HeaderLabelX5Chain int64 = 33 HeaderLabelX5T int64 = 34 @@ -97,11 +99,35 @@ func (h *ProtectedHeader) UnmarshalCBOR(data []byte) error { return nil } -// SetAlgorithm sets the algorithm value to the algorithm header. +// SetAlgorithm sets the algorithm value of the protected header. func (h ProtectedHeader) SetAlgorithm(alg Algorithm) { h[HeaderLabelAlgorithm] = alg } +// SetType sets the type of the cose object in the protected header. +func (h ProtectedHeader) SetType(typ any) (any, error) { + if !canTstr(typ) && !canUint(typ) { + return typ, errors.New("header parameter: type: require tstr / uint type") + } + h[HeaderLabelType] = typ + return typ, nil +} + +// SetCWTClaims sets the CWT Claims value of the protected header. +func (h ProtectedHeader) SetCWTClaims(claims CWTClaims) (CWTClaims, error) { + iss, hasIss := claims[1] + if hasIss && !canTstr(iss) { + return claims, errors.New("cwt claim: iss: require tstr") + } + sub, hasSub := claims[2] + if hasSub && !canTstr(sub) { + return claims, errors.New("cwt claim: sub: require tstr") + } + // TODO: validate claims, other claims + h[HeaderLabelCWTClaims] = claims + return claims, nil +} + // Algorithm gets the algorithm value from the algorithm header. func (h ProtectedHeader) Algorithm() (Algorithm, error) { value, ok := h[HeaderLabelAlgorithm] @@ -460,8 +486,8 @@ func validateHeaderParameters(h map[any]any, protected bool) error { // Reference: https://datatracker.ietf.org/doc/html/rfc8152#section-3.1 switch label { case HeaderLabelAlgorithm: - _, is_alg := value.(Algorithm) - if !is_alg && !canInt(value) && !canTstr(value) { + _, isAlg := value.(Algorithm) + if !isAlg && !canInt(value) && !canTstr(value) { return errors.New("header parameter: alg: require int / tstr type") } case HeaderLabelCritical: @@ -471,12 +497,31 @@ func validateHeaderParameters(h map[any]any, protected bool) error { if err := ensureCritical(value, h); err != nil { return fmt.Errorf("header parameter: crit: %w", err) } + case HeaderLabelType: + isTstr := canTstr(value) + if !isTstr && !canUint(value) { + return errors.New("header parameter: type: require tstr / uint type") + } + if isTstr { + v := value.(string) + if len(v) == 0 { + return errors.New("header parameter: type: require non-empty string") + } + if v[0] == ' ' || v[len(v)-1] == ' ' { + return errors.New("header parameter: type: require no leading/trailing whitespace") + } + // Basic check that the content type is of form type/subtype. + // We don't check the precise definition though (RFC 6838 Section 4.2). + if strings.Count(v, "/") != 1 { + return errors.New("header parameter: type: require text of form type/subtype") + } + } case HeaderLabelContentType: - is_tstr := canTstr(value) - if !is_tstr && !canUint(value) { + isTstr := canTstr(value) + if !isTstr && !canUint(value) { return errors.New("header parameter: content type: require tstr / uint type") } - if is_tstr { + if isTstr { v := value.(string) if len(v) == 0 { return errors.New("header parameter: content type: require non-empty string")