Skip to content

Commit a633822

Browse files
authored
feat: cose hash envelope (#212)
Signed-off-by: Shiwei Zhang <[email protected]>
1 parent eb9cdec commit a633822

File tree

7 files changed

+891
-11
lines changed

7 files changed

+891
-11
lines changed

algorithm.go

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import (
55
"strconv"
66
)
77

8-
// Algorithms supported by this library.
8+
// Signature algorithms supported by this library.
99
//
1010
// When using an algorithm which requires hashing,
1111
// make sure the associated hash function is linked to the binary.
@@ -42,12 +42,9 @@ const (
4242

4343
// PureEdDSA by RFC 8152.
4444
AlgorithmEdDSA Algorithm = -8
45-
46-
// Reserved value.
47-
AlgorithmReserved Algorithm = 0
4845
)
4946

50-
// Algorithms known, but not supported by this library.
47+
// Signature algorithms known, but not supported by this library.
5148
//
5249
// Signers and Verifiers requiring the algorithms below are not
5350
// directly supported by this library. They need to be provided
@@ -66,6 +63,21 @@ const (
6663
AlgorithmRS512 Algorithm = -259
6764
)
6865

66+
// Hash algorithms by RFC 9054.
67+
const (
68+
// SHA-256 by RFC 9054.
69+
AlgorithmSHA256 Algorithm = -16
70+
71+
// SHA-384 by RFC 9054.
72+
AlgorithmSHA384 Algorithm = -43
73+
74+
// SHA-512 by RFC 9054.
75+
AlgorithmSHA512 Algorithm = -44
76+
)
77+
78+
// AlgorithmReserved represents a reserved algorithm value by RFC 9053.
79+
const AlgorithmReserved Algorithm = 0
80+
6981
// Algorithm represents an IANA algorithm entry in the COSE Algorithms registry.
7082
//
7183
// # See Also
@@ -102,6 +114,12 @@ func (a Algorithm) String() string {
102114
return "EdDSA"
103115
case AlgorithmReserved:
104116
return "Reserved"
117+
case AlgorithmSHA256:
118+
return "SHA-256"
119+
case AlgorithmSHA384:
120+
return "SHA-384"
121+
case AlgorithmSHA512:
122+
return "SHA-512"
105123
default:
106124
return "Algorithm(" + strconv.FormatInt(int64(a), 10) + ")"
107125
}
@@ -111,11 +129,11 @@ func (a Algorithm) String() string {
111129
// library.
112130
func (a Algorithm) hashFunc() crypto.Hash {
113131
switch a {
114-
case AlgorithmPS256, AlgorithmES256:
132+
case AlgorithmPS256, AlgorithmES256, AlgorithmSHA256:
115133
return crypto.SHA256
116-
case AlgorithmPS384, AlgorithmES384:
134+
case AlgorithmPS384, AlgorithmES384, AlgorithmSHA384:
117135
return crypto.SHA384
118-
case AlgorithmPS512, AlgorithmES512:
136+
case AlgorithmPS512, AlgorithmES512, AlgorithmSHA512:
119137
return crypto.SHA512
120138
default:
121139
return 0

algorithm_test.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ func TestAlgorithm_String(t *testing.T) {
2626
{AlgorithmES512, "ES512"},
2727
{AlgorithmEdDSA, "EdDSA"},
2828
{AlgorithmReserved, "Reserved"},
29+
{AlgorithmSHA256, "SHA-256"},
30+
{AlgorithmSHA384, "SHA-384"},
31+
{AlgorithmSHA512, "SHA-512"},
2932
{7, "Algorithm(7)"},
3033
}
3134
for _, tt := range tests {
@@ -37,6 +40,36 @@ func TestAlgorithm_String(t *testing.T) {
3740
}
3841
}
3942

43+
func TestAlgorithm_hashFunc(t *testing.T) {
44+
tests := []struct {
45+
alg Algorithm
46+
want crypto.Hash
47+
}{
48+
{AlgorithmPS256, crypto.SHA256},
49+
{AlgorithmPS384, crypto.SHA384},
50+
{AlgorithmPS512, crypto.SHA512},
51+
{AlgorithmRS256, 0}, // crypto.SHA256 but not supported as intended
52+
{AlgorithmRS384, 0}, // crypto.SHA384 but not supported as intended
53+
{AlgorithmRS512, 0}, // crypto.SHA512 but not supported as intended
54+
{AlgorithmES256, crypto.SHA256},
55+
{AlgorithmES384, crypto.SHA384},
56+
{AlgorithmES512, crypto.SHA512},
57+
{AlgorithmEdDSA, 0},
58+
{AlgorithmReserved, 0},
59+
{AlgorithmSHA256, crypto.SHA256},
60+
{AlgorithmSHA384, crypto.SHA384},
61+
{AlgorithmSHA512, crypto.SHA512},
62+
{7, 0},
63+
}
64+
for _, tt := range tests {
65+
t.Run(tt.alg.String(), func(t *testing.T) {
66+
if got := tt.alg.hashFunc(); !reflect.DeepEqual(got, tt.want) {
67+
t.Errorf("Algorithm.hashFunc() = %v, want %v", got, tt.want)
68+
}
69+
})
70+
}
71+
}
72+
4073
func TestAlgorithm_computeHash(t *testing.T) {
4174
// run tests
4275
data := []byte("hello world")

example_test.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -480,3 +480,66 @@ func ExampleCountersignature() {
480480
// signature countersignature verified
481481
// verification error as expected
482482
}
483+
484+
// This example demonstrates signing and verifying COSE Hash Envelope.
485+
//
486+
// Reference: https://www.ietf.org/archive/id/draft-ietf-cose-hash-envelope-05.html
487+
//
488+
// Notice: The COSE Hash Envelope API is EXPERIMENTAL and may be changed or
489+
// removed in a later release.
490+
func Example_hashEnvelope() {
491+
// create message to be signed
492+
digested := sha512.Sum512([]byte("hello world"))
493+
payload := cose.HashEnvelopePayload{
494+
HashAlgorithm: cose.AlgorithmSHA512,
495+
HashValue: digested[:],
496+
PreimageContentType: "text/plain",
497+
Location: "urn:example:location",
498+
}
499+
500+
// create a signer
501+
privateKey, err := ecdsa.GenerateKey(elliptic.P521(), rand.Reader)
502+
if err != nil {
503+
panic(err)
504+
}
505+
signer, err := cose.NewSigner(cose.AlgorithmES512, privateKey)
506+
if err != nil {
507+
panic(err)
508+
}
509+
510+
// sign message
511+
sig, err := cose.SignHashEnvelope(rand.Reader, signer, cose.Headers{
512+
Protected: cose.ProtectedHeader{
513+
cose.HeaderLabelAlgorithm: cose.AlgorithmES512,
514+
},
515+
}, payload)
516+
if err != nil {
517+
panic(err)
518+
}
519+
fmt.Println("message signed")
520+
521+
// create a verifier from a trusted public key
522+
publicKey := privateKey.Public()
523+
verifier, err := cose.NewVerifier(cose.AlgorithmES512, publicKey)
524+
if err != nil {
525+
panic(err)
526+
}
527+
528+
// verify message
529+
msg, err := cose.VerifyHashEnvelope(verifier, sig)
530+
if err != nil {
531+
panic(err)
532+
}
533+
fmt.Println("message verified")
534+
535+
// check payload
536+
fmt.Printf("payload hash: %v: %x\n", msg.Headers.Protected[cose.HeaderLabelPayloadHashAlgorithm], msg.Payload)
537+
fmt.Println("payload content type:", msg.Headers.Protected[cose.HeaderLabelPayloadPreimageContentType])
538+
fmt.Println("payload location:", msg.Headers.Protected[cose.HeaderLabelPayloadLocation])
539+
// Output:
540+
// message signed
541+
// message verified
542+
// payload hash: SHA-512: 309ecc489c12d6eb4cc40f50c902f2b4d0ed77ee511a7c7a9bcd3ca86d4cd86f989dd35bc5ff499670da34255b45b0cfd830e81f605dcf7dc5542e93ae9cd76f
543+
// payload content type: text/plain
544+
// payload location: urn:example:location
545+
}

hash_envelope.go

Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
package cose
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"io"
7+
"maps"
8+
)
9+
10+
// HashEnvelopePayload indicates the payload of a Hash_Envelope object.
11+
// It is used by the [SignHashEnvelope] function.
12+
//
13+
// # Experimental
14+
//
15+
// Notice: The COSE Hash Envelope API is EXPERIMENTAL and may be changed or
16+
// removed in a later release.
17+
type HashEnvelopePayload struct {
18+
// HashAlgorithm is the hash algorithm used to produce the hash value.
19+
HashAlgorithm Algorithm
20+
21+
// HashValue is the hash value of the payload.
22+
HashValue []byte
23+
24+
// PreimageContentType is the content type of the data that has been hashed.
25+
// The value is either an unsigned integer (RFC 7252 Section 12.3) or a
26+
// string (RFC 9110 Section 8.3).
27+
// This field is optional.
28+
//
29+
// References:
30+
// - https://www.iana.org/assignments/core-parameters/core-parameters.xhtml
31+
// - https://www.iana.org/assignments/media-types/media-types.xhtml
32+
PreimageContentType any // uint / string
33+
34+
// Location is the location of the hash value in the payload.
35+
// This field is optional.
36+
Location string
37+
}
38+
39+
// SignHashEnvelope signs a [Sign1Message] using the provided [Signer] and
40+
// produces a Hash_Envelope object.
41+
//
42+
// Hash_Envelope_Protected_Header = {
43+
// ? &(alg: 1) => int,
44+
// &(payload_hash_alg: 258) => int
45+
// &(payload_preimage_content_type: 259) => uint / tstr
46+
// ? &(payload_location: 260) => tstr
47+
// * int / tstr => any
48+
// }
49+
//
50+
// Hash_Envelope_Unprotected_Header = {
51+
// * int / tstr => any
52+
// }
53+
//
54+
// Hash_Envelope_as_COSE_Sign1 = [
55+
// protected : bstr .cbor Hash_Envelope_Protected_Header,
56+
// unprotected : Hash_Envelope_Unprotected_Header,
57+
// payload: bstr / nil,
58+
// signature : bstr
59+
// ]
60+
//
61+
// Hash_Envelope = #6.18(Hash_Envelope_as_COSE_Sign1)
62+
//
63+
// Reference: https://www.ietf.org/archive/id/draft-ietf-cose-hash-envelope-05.html
64+
//
65+
// # Experimental
66+
//
67+
// Notice: The COSE Hash Envelope API is EXPERIMENTAL and may be changed or
68+
// removed in a later release.
69+
func SignHashEnvelope(rand io.Reader, signer Signer, headers Headers, payload HashEnvelopePayload) ([]byte, error) {
70+
if err := validateHash(payload.HashAlgorithm, payload.HashValue); err != nil {
71+
return nil, err
72+
}
73+
74+
headers.Protected = setHashEnvelopeProtectedHeader(headers.Protected, &payload)
75+
headers.RawProtected = nil
76+
if err := validateHashEnvelopeHeaders(&headers); err != nil {
77+
return nil, err
78+
}
79+
80+
return Sign1(rand, signer, headers, payload.HashValue, nil)
81+
}
82+
83+
// VerifyHashEnvelope verifies a Hash_Envelope object using the provided
84+
// [Verifier].
85+
// It returns the decoded [Sign1Message] if the verification is successful.
86+
//
87+
// # Experimental
88+
//
89+
// Notice: The COSE Hash Envelope API is EXPERIMENTAL and may be changed or
90+
// removed in a later release.
91+
func VerifyHashEnvelope(verifier Verifier, envelope []byte) (*Sign1Message, error) {
92+
// parse and validate the Hash_Envelope object
93+
var message Sign1Message
94+
if err := message.UnmarshalCBOR(envelope); err != nil {
95+
return nil, err
96+
}
97+
if err := validateHashEnvelopeHeaders(&message.Headers); err != nil {
98+
return nil, err
99+
}
100+
101+
// verify the Hash_Envelope object
102+
if err := message.Verify(nil, verifier); err != nil {
103+
return nil, err
104+
}
105+
106+
// cast to type Algorithm
107+
hashAlgorithm, err := message.Headers.Protected.PayloadHashAlgorithm()
108+
if err != nil {
109+
return nil, err
110+
}
111+
message.Headers.Protected[HeaderLabelPayloadHashAlgorithm] = hashAlgorithm
112+
113+
// validate the hash value
114+
if err := validateHash(hashAlgorithm, message.Payload); err != nil {
115+
return nil, err
116+
}
117+
118+
return &message, nil
119+
}
120+
121+
// validateHash checks the validity of the known hash.
122+
func validateHash(alg Algorithm, value []byte) error {
123+
hash := alg.hashFunc()
124+
if hash == 0 {
125+
return nil // no check on unsupported hash algorithms
126+
}
127+
if size := hash.Size(); size != len(value) {
128+
return fmt.Errorf("%v: size mismatch: expected %d, got %d", alg, size, len(value))
129+
}
130+
return nil
131+
}
132+
133+
// setHashEnvelopeProtectedHeader sets the protected header for a Hash_Envelope
134+
// object.
135+
func setHashEnvelopeProtectedHeader(base ProtectedHeader, payload *HashEnvelopePayload) ProtectedHeader {
136+
header := maps.Clone(base)
137+
if header == nil {
138+
header = make(ProtectedHeader)
139+
}
140+
header[HeaderLabelPayloadHashAlgorithm] = payload.HashAlgorithm
141+
if payload.PreimageContentType != nil {
142+
header[HeaderLabelPayloadPreimageContentType] = payload.PreimageContentType
143+
}
144+
if payload.Location != "" {
145+
header[HeaderLabelPayloadLocation] = payload.Location
146+
}
147+
return header
148+
}
149+
150+
// validateHashEnvelopeHeaders validates the headers of a Hash_Envelope object.
151+
// See https://www.ietf.org/archive/id/draft-ietf-cose-hash-envelope-05.html
152+
// section 4 for more details.
153+
func validateHashEnvelopeHeaders(headers *Headers) error {
154+
var foundPayloadHashAlgorithm bool
155+
for label, value := range headers.Protected {
156+
// Validate that all header labels are integers or strings.
157+
// Reference: https://datatracker.ietf.org/doc/html/rfc8152#section-1.4
158+
label, ok := normalizeLabel(label)
159+
if !ok {
160+
return errors.New("header label: require int / tstr type")
161+
}
162+
163+
switch label {
164+
case HeaderLabelContentType:
165+
return errors.New("protected header parameter: content type: not allowed")
166+
case HeaderLabelPayloadHashAlgorithm:
167+
_, isAlg := value.(Algorithm)
168+
if !isAlg && !canInt(value) {
169+
return errors.New("protected header parameter: payload hash alg: require int type")
170+
}
171+
foundPayloadHashAlgorithm = true
172+
case HeaderLabelPayloadPreimageContentType:
173+
if !canUint(value) && !canTstr(value) {
174+
return errors.New("protected header parameter: payload preimage content type: require uint / tstr type")
175+
}
176+
case HeaderLabelPayloadLocation:
177+
if !canTstr(value) {
178+
return errors.New("protected header parameter: payload location: require tstr type")
179+
}
180+
}
181+
}
182+
if !foundPayloadHashAlgorithm {
183+
return errors.New("protected header parameter: payload hash alg: required")
184+
}
185+
186+
for label := range headers.Unprotected {
187+
// Validate that all header labels are integers or strings.
188+
// Reference: https://datatracker.ietf.org/doc/html/rfc8152#section-1.4
189+
label, ok := normalizeLabel(label)
190+
if !ok {
191+
return errors.New("header label: require int / tstr type")
192+
}
193+
194+
switch label {
195+
case HeaderLabelContentType:
196+
return errors.New("unprotected header parameter: content type: not allowed")
197+
case HeaderLabelPayloadHashAlgorithm:
198+
return errors.New("unprotected header parameter: payload hash alg: not allowed")
199+
case HeaderLabelPayloadPreimageContentType:
200+
return errors.New("unprotected header parameter: payload preimage content type: not allowed")
201+
case HeaderLabelPayloadLocation:
202+
return errors.New("unprotected header parameter: payload location: not allowed")
203+
}
204+
}
205+
206+
return nil
207+
}

0 commit comments

Comments
 (0)