Skip to content
34 changes: 26 additions & 8 deletions algorithm.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import (
"strconv"
)

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

// PureEdDSA by RFC 8152.
AlgorithmEdDSA Algorithm = -8

// Reserved value.
AlgorithmReserved Algorithm = 0
)

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

// Hash algorithms by RFC 9054.
const (
// SHA-256 by RFC 9054.
AlgorithmSHA256 Algorithm = -16

// SHA-384 by RFC 9054.
AlgorithmSHA384 Algorithm = -43

// SHA-512 by RFC 9054.
AlgorithmSHA512 Algorithm = -44
)

// AlgorithmReserved represents a reserved algorithm value by RFC 9053.
const AlgorithmReserved Algorithm = 0

// Algorithm represents an IANA algorithm entry in the COSE Algorithms registry.
//
// # See Also
Expand Down Expand Up @@ -102,6 +114,12 @@ func (a Algorithm) String() string {
return "EdDSA"
case AlgorithmReserved:
return "Reserved"
case AlgorithmSHA256:
return "SHA-256"
case AlgorithmSHA384:
return "SHA-384"
case AlgorithmSHA512:
return "SHA-512"
default:
return "Algorithm(" + strconv.FormatInt(int64(a), 10) + ")"
}
Expand All @@ -111,11 +129,11 @@ func (a Algorithm) String() string {
// library.
func (a Algorithm) hashFunc() crypto.Hash {
switch a {
case AlgorithmPS256, AlgorithmES256:
case AlgorithmPS256, AlgorithmES256, AlgorithmSHA256:
return crypto.SHA256
case AlgorithmPS384, AlgorithmES384:
case AlgorithmPS384, AlgorithmES384, AlgorithmSHA384:
return crypto.SHA384
case AlgorithmPS512, AlgorithmES512:
case AlgorithmPS512, AlgorithmES512, AlgorithmSHA512:
return crypto.SHA512
default:
return 0
Expand Down
33 changes: 33 additions & 0 deletions algorithm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ func TestAlgorithm_String(t *testing.T) {
{AlgorithmES512, "ES512"},
{AlgorithmEdDSA, "EdDSA"},
{AlgorithmReserved, "Reserved"},
{AlgorithmSHA256, "SHA-256"},
{AlgorithmSHA384, "SHA-384"},
{AlgorithmSHA512, "SHA-512"},
{7, "Algorithm(7)"},
}
for _, tt := range tests {
Expand All @@ -37,6 +40,36 @@ func TestAlgorithm_String(t *testing.T) {
}
}

func TestAlgorithm_hashFunc(t *testing.T) {
tests := []struct {
alg Algorithm
want crypto.Hash
}{
{AlgorithmPS256, crypto.SHA256},
{AlgorithmPS384, crypto.SHA384},
{AlgorithmPS512, crypto.SHA512},
{AlgorithmRS256, 0}, // crypto.SHA256 but not supported as intended
{AlgorithmRS384, 0}, // crypto.SHA384 but not supported as intended
{AlgorithmRS512, 0}, // crypto.SHA512 but not supported as intended
{AlgorithmES256, crypto.SHA256},
{AlgorithmES384, crypto.SHA384},
{AlgorithmES512, crypto.SHA512},
{AlgorithmEdDSA, 0},
{AlgorithmReserved, 0},
{AlgorithmSHA256, crypto.SHA256},
{AlgorithmSHA384, crypto.SHA384},
{AlgorithmSHA512, crypto.SHA512},
{7, 0},
}
for _, tt := range tests {
t.Run(tt.alg.String(), func(t *testing.T) {
if got := tt.alg.hashFunc(); !reflect.DeepEqual(got, tt.want) {
t.Errorf("Algorithm.hashFunc() = %v, want %v", got, tt.want)
}
})
}
}

func TestAlgorithm_computeHash(t *testing.T) {
// run tests
data := []byte("hello world")
Expand Down
63 changes: 63 additions & 0 deletions example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -480,3 +480,66 @@ func ExampleCountersignature() {
// signature countersignature verified
// verification error as expected
}

// This example demonstrates signing and verifying COSE Hash Envelope.
//
// Reference: https://www.ietf.org/archive/id/draft-ietf-cose-hash-envelope-05.html
//
// Notice: The COSE Hash Envelope API is EXPERIMENTAL and may be changed or
// removed in a later release.
func Example_hashEnvelope() {
// create message to be signed
digested := sha512.Sum512([]byte("hello world"))
payload := cose.HashEnvelopePayload{
HashAlgorithm: cose.AlgorithmSHA512,
HashValue: digested[:],
PreimageContentType: "text/plain",
Location: "urn:example:location",
}

// 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
sig, err := cose.SignHashEnvelope(rand.Reader, signer, cose.Headers{
Protected: cose.ProtectedHeader{
cose.HeaderLabelAlgorithm: cose.AlgorithmES512,
},
}, payload)
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
msg, err := cose.VerifyHashEnvelope(verifier, sig)
if err != nil {
panic(err)
}
fmt.Println("message verified")

// check payload
fmt.Printf("payload hash: %v: %x\n", msg.Headers.Protected[cose.HeaderLabelPayloadHashAlgorithm], msg.Payload)
fmt.Println("payload content type:", msg.Headers.Protected[cose.HeaderLabelPayloadPreimageContentType])
fmt.Println("payload location:", msg.Headers.Protected[cose.HeaderLabelPayloadLocation])
// Output:
// message signed
// message verified
// payload hash: SHA-512: 309ecc489c12d6eb4cc40f50c902f2b4d0ed77ee511a7c7a9bcd3ca86d4cd86f989dd35bc5ff499670da34255b45b0cfd830e81f605dcf7dc5542e93ae9cd76f
// payload content type: text/plain
// payload location: urn:example:location
}
207 changes: 207 additions & 0 deletions hash_envelope.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
package cose

import (
"errors"
"fmt"
"io"
"maps"
)

// HashEnvelopePayload indicates the payload of a Hash_Envelope object.
// It is used by the [SignHashEnvelope] function.
//
// # Experimental
//
// Notice: The COSE Hash Envelope API is EXPERIMENTAL and may be changed or
// removed in a later release.
type HashEnvelopePayload struct {
// HashAlgorithm is the hash algorithm used to produce the hash value.
HashAlgorithm Algorithm

// HashValue is the hash value of the payload.
HashValue []byte

// PreimageContentType is the content type of the data that has been hashed.
// The value is either an unsigned integer (RFC 7252 Section 12.3) or a
// string (RFC 9110 Section 8.3).
// This field is optional.
//
// References:
// - https://www.iana.org/assignments/core-parameters/core-parameters.xhtml
// - https://www.iana.org/assignments/media-types/media-types.xhtml
PreimageContentType any // uint / string

// Location is the location of the hash value in the payload.
// This field is optional.
Location string
}

// SignHashEnvelope signs a [Sign1Message] using the provided [Signer] and
// produces a Hash_Envelope object.
//
// Hash_Envelope_Protected_Header = {
// ? &(alg: 1) => int,
// &(payload_hash_alg: 258) => int
// &(payload_preimage_content_type: 259) => uint / tstr
// ? &(payload_location: 260) => tstr
// * int / tstr => any
// }
//
// Hash_Envelope_Unprotected_Header = {
// * int / tstr => any
// }
//
// Hash_Envelope_as_COSE_Sign1 = [
// protected : bstr .cbor Hash_Envelope_Protected_Header,
// unprotected : Hash_Envelope_Unprotected_Header,
// payload: bstr / nil,
// signature : bstr
// ]
//
// Hash_Envelope = #6.18(Hash_Envelope_as_COSE_Sign1)
//
// Reference: https://www.ietf.org/archive/id/draft-ietf-cose-hash-envelope-05.html
//
// # Experimental
//
// Notice: The COSE Hash Envelope API is EXPERIMENTAL and may be changed or
// removed in a later release.
func SignHashEnvelope(rand io.Reader, signer Signer, headers Headers, payload HashEnvelopePayload) ([]byte, error) {
if err := validateHash(payload.HashAlgorithm, payload.HashValue); err != nil {
return nil, err
}

headers.Protected = setHashEnvelopeProtectedHeader(headers.Protected, &payload)
headers.RawProtected = nil
if err := validateHashEnvelopeHeaders(&headers); err != nil {
return nil, err
}

return Sign1(rand, signer, headers, payload.HashValue, nil)
}

// VerifyHashEnvelope verifies a Hash_Envelope object using the provided
// [Verifier].
// It returns the decoded [Sign1Message] if the verification is successful.
//
// # Experimental
//
// Notice: The COSE Hash Envelope API is EXPERIMENTAL and may be changed or
// removed in a later release.
func VerifyHashEnvelope(verifier Verifier, envelope []byte) (*Sign1Message, error) {
// parse and validate the Hash_Envelope object
var message Sign1Message
if err := message.UnmarshalCBOR(envelope); err != nil {
return nil, err
}
if err := validateHashEnvelopeHeaders(&message.Headers); err != nil {
return nil, err
}

// verify the Hash_Envelope object
if err := message.Verify(nil, verifier); err != nil {
return nil, err
}

// cast to type Algorithm
hashAlgorithm, err := message.Headers.Protected.PayloadHashAlgorithm()
if err != nil {
return nil, err
}
message.Headers.Protected[HeaderLabelPayloadHashAlgorithm] = hashAlgorithm

// validate the hash value
if err := validateHash(hashAlgorithm, message.Payload); err != nil {
return nil, err
}

return &message, nil
}

// validateHash checks the validity of the known hash.
func validateHash(alg Algorithm, value []byte) error {
hash := alg.hashFunc()
if hash == 0 {
return nil // no check on unsupported hash algorithms
}
if size := hash.Size(); size != len(value) {
return fmt.Errorf("%v: size mismatch: expected %d, got %d", alg, size, len(value))
}
return nil
}

// setHashEnvelopeProtectedHeader sets the protected header for a Hash_Envelope
// object.
func setHashEnvelopeProtectedHeader(base ProtectedHeader, payload *HashEnvelopePayload) ProtectedHeader {
header := maps.Clone(base)
if header == nil {
header = make(ProtectedHeader)
}
header[HeaderLabelPayloadHashAlgorithm] = payload.HashAlgorithm
if payload.PreimageContentType != nil {
header[HeaderLabelPayloadPreimageContentType] = payload.PreimageContentType
}
if payload.Location != "" {
header[HeaderLabelPayloadLocation] = payload.Location
}
return header
}

// validateHashEnvelopeHeaders validates the headers of a Hash_Envelope object.
// See https://www.ietf.org/archive/id/draft-ietf-cose-hash-envelope-05.html
// section 4 for more details.
func validateHashEnvelopeHeaders(headers *Headers) error {
var foundPayloadHashAlgorithm bool
for label, value := range headers.Protected {
// Validate that all header labels are integers or strings.
// Reference: https://datatracker.ietf.org/doc/html/rfc8152#section-1.4
label, ok := normalizeLabel(label)
if !ok {
return errors.New("header label: require int / tstr type")
}

switch label {
case HeaderLabelContentType:
return errors.New("protected header parameter: content type: not allowed")
case HeaderLabelPayloadHashAlgorithm:
_, isAlg := value.(Algorithm)
if !isAlg && !canInt(value) {
return errors.New("protected header parameter: payload hash alg: require int type")
}
foundPayloadHashAlgorithm = true
case HeaderLabelPayloadPreimageContentType:
if !canUint(value) && !canTstr(value) {
return errors.New("protected header parameter: payload preimage content type: require uint / tstr type")
}
case HeaderLabelPayloadLocation:
if !canTstr(value) {
return errors.New("protected header parameter: payload location: require tstr type")
}
}
}
if !foundPayloadHashAlgorithm {
return errors.New("protected header parameter: payload hash alg: required")
}

for label := range headers.Unprotected {
// Validate that all header labels are integers or strings.
// Reference: https://datatracker.ietf.org/doc/html/rfc8152#section-1.4
label, ok := normalizeLabel(label)
if !ok {
return errors.New("header label: require int / tstr type")
}

switch label {
case HeaderLabelContentType:
return errors.New("unprotected header parameter: content type: not allowed")
case HeaderLabelPayloadHashAlgorithm:
return errors.New("unprotected header parameter: payload hash alg: not allowed")
case HeaderLabelPayloadPreimageContentType:
return errors.New("unprotected header parameter: payload preimage content type: not allowed")
case HeaderLabelPayloadLocation:
return errors.New("unprotected header parameter: payload location: not allowed")
}
}

return nil
}
Loading