Skip to content
Draft
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
130 changes: 130 additions & 0 deletions cmd/attach_receipt/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
// attach_receipt rewrites a COSE_Sign1 envelope's unprotected `receipts`
// header (label 394), replacing it with either a single raw receipt blob or
// with the receipts copied from a donor COSE_Sign1 envelope. The protected
// header, payload, and signature of the input envelope are preserved
// byte-for-byte by editing the outer CBOR array in place.
package main

import (
"flag"
"fmt"
"os"

"github.com/fxamacker/cbor/v2"

"github.com/Microsoft/cosesign1go/pkg/cosesign1"
)

func main() {
in := flag.String("in", "", "input COSE_Sign1 envelope file")
out := flag.String("out", "", "output COSE_Sign1 envelope file")
donor := flag.String("donor", "", "donor COSE_Sign1 envelope to steal receipts from")
receipt := flag.String("receipt", "", "raw COSE_Sign1 receipt blob to attach (alternative to --donor)")
flag.Parse()

if *in == "" || *out == "" || (*donor == "") == (*receipt == "") {
fmt.Fprintln(os.Stderr, "usage: attach_receipt -in IN.cose -out OUT.cose (-donor DONOR.cose | -receipt R.bin)")
os.Exit(2)
}

inBytes, err := os.ReadFile(*in)
check(err)

var receipts []interface{}
if *donor != "" {
dBytes, err := os.ReadFile(*donor)
check(err)
receipts, err = extractReceipts(dBytes)
check(err)
if len(receipts) == 0 {
die("donor envelope has no receipts in unprotected header")
}
} else {
rBytes, err := os.ReadFile(*receipt)
check(err)
receipts = []interface{}{rBytes}
}

patched, err := replaceReceipts(inBytes, receipts)
check(err)
check(os.WriteFile(*out, patched, 0o644))
}

// rawSign1 is a COSE_Sign1 represented as a 4-element CBOR array, keeping
// each element as a RawMessage so the protected bstr, payload, and signature
// can be preserved verbatim across re-encoding.
type rawSign1 struct {
_ struct{} `cbor:",toarray"`
Protected cbor.RawMessage
Unprotected map[interface{}]interface{}
Payload cbor.RawMessage
Signature cbor.RawMessage
}

// decodeSign1 parses a COSE_Sign1, stripping the optional CBOR tag (18).
func decodeSign1(data []byte) (rawSign1, bool, error) {
// COSE_Sign1 tag is encoded as 0xd2 (major type 6, value 18).
hadTag := len(data) > 0 && data[0] == 0xd2
var msg rawSign1
if err := cbor.Unmarshal(data, &msg); err != nil {
return rawSign1{}, false, fmt.Errorf("decoding COSE_Sign1: %w", err)
}
return msg, hadTag, nil
}

func extractReceipts(data []byte) ([]interface{}, error) {
msg, _, err := decodeSign1(data)
if err != nil {
return nil, err
}
val, ok := msg.Unprotected[int64(cosesign1.COSE_Header_Receipts)]
if !ok {
// fxamacker/cbor may decode small int keys as uint64.
val, ok = msg.Unprotected[uint64(cosesign1.COSE_Header_Receipts)]
}
if !ok {
return nil, nil
}
arr, ok := val.([]interface{})
if !ok {
return nil, fmt.Errorf("receipts header is not an array (got %T)", val)
}
return arr, nil
}

func replaceReceipts(data []byte, receipts []interface{}) ([]byte, error) {
msg, hadTag, err := decodeSign1(data)
if err != nil {
return nil, err
}
// Delete any existing receipts entry (under either int key type) then set.
delete(msg.Unprotected, int64(cosesign1.COSE_Header_Receipts))
delete(msg.Unprotected, uint64(cosesign1.COSE_Header_Receipts))
msg.Unprotected[int64(cosesign1.COSE_Header_Receipts)] = receipts

encOpts := cbor.CoreDetEncOptions()
encOpts.Sort = cbor.SortNone
em, err := encOpts.EncMode()
if err != nil {
return nil, err
}
body, err := em.Marshal(msg)
if err != nil {
return nil, fmt.Errorf("encoding patched COSE_Sign1: %w", err)
}
if hadTag {
return em.Marshal(cbor.Tag{Number: 18, Content: cbor.RawMessage(body)})
}
return body, nil
}

func check(err error) {
if err != nil {
die(err.Error())
}
}

func die(msg string) {
fmt.Fprintln(os.Stderr, "attach_receipt:", msg)
os.Exit(1)
}
154 changes: 154 additions & 0 deletions cmd/sign1util/ccf_keyfetch.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
package main

import (
"crypto"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/tls"
"crypto/x509"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"math/big"
"net/http"
"time"

"github.com/Microsoft/cosesign1go/pkg/cosesign1"
)

// jwk is a minimal JSON Web Key representation used to parse CCF transparency
// service /jwks responses.
type jwk struct {
Kty string `json:"kty"`
Crv string `json:"crv"`
Kid string `json:"kid"`
X string `json:"x"`
Y string `json:"y"`
}

type jwkSet struct {
Keys []jwk `json:"keys"`
}

func jwkToPublicKey(k jwk) (crypto.PublicKey, error) {
if k.Kty != "EC" {
return nil, fmt.Errorf("unsupported kty %q", k.Kty)
}
var curve elliptic.Curve
switch k.Crv {
case "P-256":
curve = elliptic.P256()
case "P-384":
curve = elliptic.P384()
case "P-521":
curve = elliptic.P521()
default:
return nil, fmt.Errorf("unsupported curve %q", k.Crv)
}
xBytes, err := base64.RawURLEncoding.DecodeString(k.X)
if err != nil {
return nil, fmt.Errorf("decoding x: %w", err)
}
yBytes, err := base64.RawURLEncoding.DecodeString(k.Y)
if err != nil {
return nil, fmt.Errorf("decoding y: %w", err)
}
return &ecdsa.PublicKey{
Curve: curve,
X: new(big.Int).SetBytes(xBytes),
Y: new(big.Int).SetBytes(yBytes),
}, nil
}

// CertVerifier is invoked with the leaf certificate presented by a CCF node
// during TLS handshake. Returning nil accepts the certificate; returning an
// error aborts the connection.
type CertVerifier func(issuer string, cert *x509.Certificate) error

// fetchIssuerJWKS GETs https://<issuer>/jwks and returns the keys keyed by
// their `kid`. If verifyCert is not nil, the leaf certificate presented by the
// server is passed to verifyCert.
func fetchIssuerJWKS(issuer string, verifyCert CertVerifier) (map[string]crypto.PublicKey, error) {
url := "https://" + issuer + "/jwks"
tlsConfig := &tls.Config{InsecureSkipVerify: true} //nolint:gosec // CCF uses self-signed certs that are supposed to be validated via attestation, and so will never pass the normal verification.
Comment thread
micromaomao marked this conversation as resolved.
Dismissed
if verifyCert != nil {
tlsConfig.VerifyPeerCertificate = func(rawCerts [][]byte, _ [][]*x509.Certificate) error {
if len(rawCerts) == 0 {
return fmt.Errorf("server presented no certificate")
}
cert, err := x509.ParseCertificate(rawCerts[0])
if err != nil {
return fmt.Errorf("parsing server certificate: %w", err)
}
return verifyCert(issuer, cert)
}
}
client := &http.Client{
Transport: &http.Transport{
TLSClientConfig: tlsConfig,
},
Timeout: 10 * time.Second,
}
Comment thread
micromaomao marked this conversation as resolved.
resp, err := client.Get(url)
if err != nil {
return nil, fmt.Errorf("GET %s: %w", url, err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("GET %s: status %d", url, resp.StatusCode)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("reading %s: %w", url, err)
}
var set jwkSet
if err := json.Unmarshal(body, &set); err != nil {
return nil, fmt.Errorf("parsing %s: %w", url, err)
}
Comment on lines +101 to +108
out := make(map[string]crypto.PublicKey, len(set.Keys))
for i, k := range set.Keys {
pub, err := jwkToPublicKey(k)
if err != nil {
return nil, fmt.Errorf("key %d (kid=%s): %w", i, k.Kid, err)
}
if existingKey, exists := out[k.Kid]; exists {
// Equal is implemented for all crypto.PublicKey types in std
eq, ok := existingKey.(interface{ Equal(crypto.PublicKey) bool })
if !ok || !eq.Equal(pub) {
return nil, fmt.Errorf("conflicting kid %s seen in JWKS from %s", k.Kid, url)
}
continue
}
out[k.Kid] = pub
}
return out, nil
}

// fetchCCFReceiptKeys returns a kid->PublicKey map by fetching the JWKS for
// each unique receipt issuer. If not nil, verifyCert is invoked with the leaf
// certificate presented by each issuer.
func fetchCCFReceiptKeys(receipts []cosesign1.ParsedCOSEReceipt, verifyCert CertVerifier) (map[string]crypto.PublicKey, error) {
seen := map[string]bool{}
keys := map[string]crypto.PublicKey{}
for _, r := range receipts {
if r.Issuer == "" {
return nil, fmt.Errorf("receipt has no issuer; cannot fetch JWKS")
}
if seen[r.Issuer] {
continue
}
seen[r.Issuer] = true
issuerKeys, err := fetchIssuerJWKS(r.Issuer, verifyCert)
if err != nil {
return nil, err
}
for kid, k := range issuerKeys {
if _, exists := keys[kid]; exists {
return nil, fmt.Errorf("Issuer %s JWKS contains kid %s which is already present from another issuer", r.Issuer, kid)
}
keys[kid] = k
}
}
return keys, nil
}
Loading
Loading