Created
February 29, 2024 12:14
-
-
Save ivarprudnikov/a368f5a2c4c9ac4eaaa1e45409ed1425 to your computer and use it in GitHub Desktop.
Create COSE_Sign1 signature envelope in Go and sign with a key in Azure KeyVault
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
module something.com/whatever | |
go 1.19 | |
require github.com/veraison/go-cose v1.1.1-0.20240126165338-2300d5c96dbd | |
require ( | |
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.2 // indirect | |
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.5.1 // indirect | |
github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.2 // indirect | |
github.com/Azure/azure-sdk-for-go/sdk/keyvault/azkeys v0.10.0 // indirect | |
github.com/Azure/azure-sdk-for-go/sdk/keyvault/internal v0.7.1 // indirect | |
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azcertificates v1.1.0 // indirect | |
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.1.0 // indirect | |
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0 // indirect | |
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 // indirect | |
github.com/fxamacker/cbor/v2 v2.5.0 // indirect | |
github.com/golang-jwt/jwt/v5 v5.2.0 // indirect | |
github.com/google/uuid v1.6.0 // indirect | |
github.com/kylelemons/godebug v1.1.0 // indirect | |
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect | |
github.com/x448/float16 v0.8.4 // indirect | |
golang.org/x/crypto v0.20.0 // indirect | |
golang.org/x/net v0.21.0 // indirect | |
golang.org/x/sys v0.17.0 // indirect | |
golang.org/x/text v0.14.0 // indirect | |
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package main | |
import ( | |
"context" | |
"crypto" | |
"crypto/rand" | |
"crypto/x509" | |
"encoding/pem" | |
"flag" | |
"fmt" | |
"io" | |
"net/http" | |
"os" | |
"time" | |
"github.com/Azure/azure-sdk-for-go/sdk/azidentity" | |
"github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azcertificates" | |
"github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys" | |
"github.com/veraison/go-cose" | |
) | |
// https://datatracker.ietf.org/doc/draft-ietf-scitt-architecture/ | |
const ISSUER_HEADER_KEY = int64(391) | |
const ISSUER_HEADER_FEED = int64(392) | |
const ISSUER_HEADER_REG_INFO = int64(393) | |
const HASH_ALG = cose.AlgorithmPS256 | |
var kvUrl string | |
var keyName string | |
var keyVer string | |
var payloadPath string | |
var outputFilePath string | |
func init() { | |
flag.StringVar(&kvUrl, "kv-url", "https://myvault.vault.azure.net", "Key Vault URL") | |
flag.StringVar(&keyName, "key-name", "generatedcert", "Key name") | |
flag.StringVar(&keyVer, "key-ver", "abc123", "Key version") | |
flag.StringVar(&payloadPath, "payload", "foobar.txt", "Payload path") | |
flag.StringVar(&outputFilePath, "output", "foobar.sig.cose", "Output file path") | |
} | |
func main() { | |
// parse args | |
flag.Parse() | |
pwd, _ := os.Getwd() | |
fmt.Println("pwd:", pwd) | |
fmt.Println("Key Vault URL:", kvUrl) | |
fmt.Println("Key Name:", keyName) | |
fmt.Println("Key Version:", keyVer) | |
fmt.Println("Payload Path:", payloadPath) | |
fmt.Println("Output Path:", outputFilePath) | |
// prepare keyvault digest signer | |
signer := &akvSigner{ | |
keyVaultUrl: kvUrl, | |
keyName: keyName, | |
keyVer: keyVer, | |
} | |
// read payload | |
payload, err := os.ReadFile(payloadPath) | |
if err != nil { | |
fmt.Printf("failed to read payload:\n%v\n", err) | |
os.Exit(1) | |
} | |
// sign | |
sig, err := CreateSignature(payload, "text/plain", signer) | |
if err != nil { | |
fmt.Printf("failed to create sig:\n%v\n", err) | |
os.Exit(1) | |
} | |
// save | |
err = os.WriteFile(outputFilePath, sig, 0644) | |
if err != nil { | |
fmt.Printf("failed to write sig to file:\n%v\n", err) | |
os.Exit(1) | |
} | |
} | |
// implement crypto.Signer | |
type akvSigner struct { | |
keyVaultUrl string | |
keyName string | |
keyVer string | |
} | |
func (s *akvSigner) keyClient() (*azkeys.Client, error) { | |
// create credential | |
cred, err := azidentity.NewDefaultAzureCredential(nil) | |
if err != nil { | |
return nil, err | |
} | |
// create azkeys client | |
client, err := azkeys.NewClient(s.keyVaultUrl, cred, nil) | |
if err != nil { | |
return nil, err | |
} | |
return client, nil | |
} | |
func (s *akvSigner) certClient() (*azcertificates.Client, error) { | |
// create credential | |
cred, err := azidentity.NewDefaultAzureCredential(nil) | |
if err != nil { | |
return nil, err | |
} | |
// create azkeys client | |
client, err := azcertificates.NewClient(s.keyVaultUrl, cred, nil) | |
if err != nil { | |
return nil, err | |
} | |
return client, nil | |
} | |
// certs returns the certificate chain for the signer | |
// it will attempt to download the CA from a url in the x509 extension | |
func (s *akvSigner) certs() [][]byte { | |
c, err := s.certClient() | |
if err != nil { | |
fmt.Printf("failed to init cert client:\n%v\n", err) | |
os.Exit(1) | |
} | |
// get the cert representing the signer | |
resp, err := c.GetCertificate(context.Background(), keyName, keyVer, nil) | |
if err != nil { | |
fmt.Printf("failed to get cert:\n%v\n", err) | |
os.Exit(1) | |
} | |
certs := [][]byte{resp.Certificate.CER} | |
// check if the cert contains IssuingCertificateURL | |
cert, err := x509.ParseCertificate(resp.Certificate.CER) | |
if err != nil { | |
fmt.Printf("failed to parse the cert:\n%v\n", err) | |
os.Exit(1) | |
} | |
fmt.Println(cert.IssuingCertificateURL) | |
if len(cert.IssuingCertificateURL) > 0 { | |
// get the issuing cert | |
issResp, err := http.DefaultClient.Get(cert.IssuingCertificateURL[0]) | |
if err != nil { | |
fmt.Printf("failed to download issuing cert:\n%v\n", err) | |
os.Exit(1) | |
} | |
if issResp.StatusCode >= 200 && issResp.StatusCode < 300 { | |
pemContent, err := io.ReadAll(issResp.Body) | |
if err != nil { | |
fmt.Printf("failed to read downloaded body:\n%v\n", err) | |
os.Exit(1) | |
} | |
// parse pem data | |
certData := pemContent | |
for { | |
var block *pem.Block | |
block, certData = pem.Decode(certData) | |
if block == nil { | |
break | |
} | |
certs = append(certs, block.Bytes) | |
} | |
} else { | |
fmt.Printf("failed to download issuing cert:\n%v\n", issResp.Status) | |
os.Exit(1) | |
} | |
} else { | |
fmt.Println("no issuing cert, adding itself to the chain to act as a root CA.") | |
certs = append(certs, resp.Certificate.CER) | |
} | |
return certs | |
} | |
func (s *akvSigner) Public() crypto.PublicKey { | |
certs := s.certs() | |
if len(certs) == 0 { | |
fmt.Printf("no certs found\n") | |
os.Exit(1) | |
} | |
// parse the signer cert | |
cert, err := x509.ParseCertificate(certs[0]) | |
if err != nil { | |
fmt.Printf("failed to parse the first cert in ca bundle:\n%v\n", err) | |
os.Exit(1) | |
} | |
return cert.PublicKey | |
} | |
func (s *akvSigner) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) ([]byte, error) { | |
client, err := s.keyClient() | |
if err != nil { | |
return nil, err | |
} | |
a := azkeys.SignatureAlgorithm(HASH_ALG.String()) | |
// sign content | |
sigResp, err := client.Sign(context.Background(), s.keyName, s.keyVer, azkeys.SignParameters{ | |
Algorithm: &a, | |
Value: digest, | |
}, nil) | |
if err != nil { | |
return nil, err | |
} | |
return sigResp.Result, nil | |
} | |
func CreateSignature(content []byte, contentType string, keySigner *akvSigner) ([]byte, error) { | |
// create AKV digest signer | |
signer, err := cose.NewSigner(HASH_ALG, keySigner) | |
if err != nil { | |
return nil, fmt.Errorf("failed to create signer: %w", err) | |
} | |
// create message header | |
headers := cose.Headers{ | |
Protected: cose.ProtectedHeader{ | |
cose.HeaderLabelAlgorithm: HASH_ALG, | |
cose.HeaderLabelContentType: contentType, | |
ISSUER_HEADER_FEED: "demo", | |
ISSUER_HEADER_REG_INFO: map[interface{}]interface{}{ | |
"register_by": uint64(time.Now().Add(24 * time.Hour).Unix()), | |
"sequence_no": uint64(1), | |
"issuance_ts": uint64(time.Now().Unix()), | |
}, | |
cose.HeaderLabelX5Chain: keySigner.certs(), | |
}, | |
} | |
// sign and marshal message | |
return cose.Sign1(rand.Reader, signer, headers, content, nil) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment