mirror of
https://github.com/outbackdingo/certificates.git
synced 2026-01-27 10:18:34 +00:00
Merge pull request #2382 from smallstep/herman/step-managed-device
Support managed device ID OID for `step` attestation format
This commit is contained in:
@@ -22,6 +22,7 @@ import (
|
||||
"net"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -29,12 +30,12 @@ import (
|
||||
"github.com/coreos/go-oidc/v3/oidc"
|
||||
"github.com/fxamacker/cbor/v2"
|
||||
"github.com/google/go-tpm/legacy/tpm2"
|
||||
|
||||
"github.com/smallstep/go-attestation/attest"
|
||||
"go.step.sm/crypto/jose"
|
||||
"go.step.sm/crypto/keyutil"
|
||||
"go.step.sm/crypto/pemutil"
|
||||
"go.step.sm/crypto/x509util"
|
||||
"golang.org/x/exp/slices"
|
||||
|
||||
"github.com/smallstep/certificates/acme/wire"
|
||||
"github.com/smallstep/certificates/authority/provisioner"
|
||||
@@ -1412,9 +1413,14 @@ roRcFD354g7rKfu67qFAw9gC4yi0xBTPrY95rh4/HqaUYCA/L8ldRk6H7Xk35D+W
|
||||
Vpmq2Sh/xT5HiFuhf4wJb0bK
|
||||
-----END CERTIFICATE-----`
|
||||
|
||||
// Serial number of the YubiKey, encoded as an integer.
|
||||
// https://developers.yubico.com/PIV/Introduction/PIV_attestation.html
|
||||
var oidYubicoSerialNumber = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 41482, 3, 7}
|
||||
var (
|
||||
// serial number of the YubiKey, encoded as an integer.
|
||||
// https://developers.yubico.com/PIV/Introduction/PIV_attestation.html
|
||||
oidYubicoSerialNumber = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 41482, 3, 7}
|
||||
|
||||
// custom Smallstep managed device extension carrying a device ID or serial number
|
||||
oidStepManagedDevice = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 37476, 9000, 64, 4}
|
||||
)
|
||||
|
||||
type stepAttestationData struct {
|
||||
Certificate *x509.Certificate
|
||||
@@ -1520,25 +1526,47 @@ func doStepAttestationFormat(_ context.Context, prov Provisioner, ch *Challenge,
|
||||
data := &stepAttestationData{
|
||||
Certificate: leaf,
|
||||
}
|
||||
|
||||
if data.Fingerprint, err = keyutil.Fingerprint(leaf.PublicKey); err != nil {
|
||||
return nil, WrapErrorISE(err, "error calculating key fingerprint")
|
||||
}
|
||||
for _, ext := range leaf.Extensions {
|
||||
if !ext.Id.Equal(oidYubicoSerialNumber) {
|
||||
continue
|
||||
}
|
||||
var serialNumber int
|
||||
rest, err := asn1.Unmarshal(ext.Value, &serialNumber)
|
||||
if err != nil || len(rest) > 0 {
|
||||
return nil, WrapError(ErrorBadAttestationStatementType, err, "error parsing serial number")
|
||||
}
|
||||
data.SerialNumber = strconv.Itoa(serialNumber)
|
||||
break
|
||||
|
||||
if data.SerialNumber, err = searchSerialNumber(leaf); err != nil {
|
||||
return nil, WrapErrorISE(err, "error finding serial number")
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// searchSerialNumber searches the certificate extensions, looking for a serial
|
||||
// number encoded in one of them. It is not guaranteed that a certificate contains
|
||||
// an extension carrying a serial number, so the result can be empty.
|
||||
func searchSerialNumber(cert *x509.Certificate) (string, error) {
|
||||
for _, ext := range cert.Extensions {
|
||||
if ext.Id.Equal(oidYubicoSerialNumber) {
|
||||
var serialNumber int
|
||||
rest, err := asn1.Unmarshal(ext.Value, &serialNumber)
|
||||
if err != nil || len(rest) > 0 {
|
||||
return "", WrapError(ErrorBadAttestationStatementType, err, "error parsing serial number")
|
||||
}
|
||||
return strconv.Itoa(serialNumber), nil
|
||||
}
|
||||
if ext.Id.Equal(oidStepManagedDevice) {
|
||||
type stepManagedDevice struct {
|
||||
DeviceID string
|
||||
}
|
||||
var md stepManagedDevice
|
||||
rest, err := asn1.Unmarshal(ext.Value, &md)
|
||||
if err != nil || len(rest) > 0 {
|
||||
return "", WrapError(ErrorBadAttestationStatementType, err, "error parsing serial number")
|
||||
}
|
||||
return md.DeviceID, nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// serverName determines the SNI HostName to set based on an acme.Challenge
|
||||
// for TLS-ALPN-01 challenges RFC8738 states that, if HostName is an IP, it
|
||||
// should be the ARPA address https://datatracker.ietf.org/doc/html/rfc8738#section-6.
|
||||
|
||||
@@ -31,16 +31,18 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/fxamacker/cbor/v2"
|
||||
"github.com/smallstep/certificates/authority/config"
|
||||
"github.com/smallstep/certificates/authority/provisioner"
|
||||
wireprovisioner "github.com/smallstep/certificates/authority/provisioner/wire"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"go.step.sm/crypto/jose"
|
||||
"go.step.sm/crypto/keyutil"
|
||||
"go.step.sm/crypto/minica"
|
||||
"go.step.sm/crypto/pemutil"
|
||||
"go.step.sm/crypto/x509util"
|
||||
|
||||
"github.com/smallstep/certificates/authority/config"
|
||||
"github.com/smallstep/certificates/authority/provisioner"
|
||||
wireprovisioner "github.com/smallstep/certificates/authority/provisioner/wire"
|
||||
)
|
||||
|
||||
type mockClient struct {
|
||||
@@ -200,6 +202,60 @@ func mustAttestYubikey(t *testing.T, _, keyAuthorization string, serial int) ([]
|
||||
return payload, leaf, ca.Root
|
||||
}
|
||||
|
||||
type stepManagedDevice struct {
|
||||
DeviceID string
|
||||
}
|
||||
|
||||
func mustAttestStepManagedDeviceID(t *testing.T, _, keyAuthorization, serialNumber string) ([]byte, *x509.Certificate, *x509.Certificate) {
|
||||
t.Helper()
|
||||
|
||||
ca, err := minica.New()
|
||||
require.NoError(t, err)
|
||||
|
||||
keyAuthSum := sha256.Sum256([]byte(keyAuthorization))
|
||||
|
||||
signer, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
require.NoError(t, err)
|
||||
sig, err := signer.Sign(rand.Reader, keyAuthSum[:], crypto.SHA256)
|
||||
require.NoError(t, err)
|
||||
cborSig, err := cbor.Marshal(sig)
|
||||
require.NoError(t, err)
|
||||
|
||||
v, err := asn1.Marshal(stepManagedDevice{DeviceID: serialNumber})
|
||||
require.NoError(t, err)
|
||||
|
||||
leaf, err := ca.Sign(&x509.Certificate{
|
||||
Subject: pkix.Name{CommonName: "attestation cert"},
|
||||
PublicKey: signer.Public(),
|
||||
ExtraExtensions: []pkix.Extension{
|
||||
{Id: oidStepManagedDevice, Value: v},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
attObj, err := cbor.Marshal(struct {
|
||||
Format string `json:"fmt"`
|
||||
AttStatement map[string]interface{} `json:"attStmt,omitempty"`
|
||||
}{
|
||||
Format: "step",
|
||||
AttStatement: map[string]interface{}{
|
||||
"x5c": []interface{}{leaf.Raw, ca.Intermediate.Raw},
|
||||
"alg": -7,
|
||||
"sig": cborSig,
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
payload, err := json.Marshal(struct {
|
||||
AttObj string `json:"attObj"`
|
||||
}{
|
||||
AttObj: base64.RawURLEncoding.EncodeToString(attObj),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
return payload, leaf, ca.Root
|
||||
}
|
||||
|
||||
func newWireProvisionerWithOptions(t *testing.T, options *provisioner.Options) *provisioner.ACME {
|
||||
t.Helper()
|
||||
prov := &provisioner.ACME{
|
||||
@@ -3499,9 +3555,8 @@ func Test_doAppleAttestationFormat(t *testing.T) {
|
||||
func Test_doStepAttestationFormat(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ca, err := minica.New()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
require.NoError(t, err)
|
||||
|
||||
caRoot := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: ca.Root.Raw})
|
||||
|
||||
makeLeaf := func(signer crypto.Signer, serialNumber []byte) *x509.Certificate {
|
||||
@@ -3512,63 +3567,63 @@ func Test_doStepAttestationFormat(t *testing.T) {
|
||||
{Id: oidYubicoSerialNumber, Value: serialNumber},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
require.NoError(t, err)
|
||||
return leaf
|
||||
}
|
||||
|
||||
makeLeafWithStepManagedDeviceID := func(signer crypto.Signer, serialNumber string) *x509.Certificate {
|
||||
v, err := asn1.Marshal(stepManagedDevice{DeviceID: serialNumber})
|
||||
require.NoError(t, err)
|
||||
leaf, err := ca.Sign(&x509.Certificate{
|
||||
Subject: pkix.Name{CommonName: "attestation cert"},
|
||||
PublicKey: signer.Public(),
|
||||
ExtraExtensions: []pkix.Extension{
|
||||
{Id: oidStepManagedDevice, Value: v},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
return leaf
|
||||
}
|
||||
|
||||
mustSigner := func(kty, crv string, size int) crypto.Signer {
|
||||
s, err := keyutil.GenerateSigner(kty, crv, size)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
require.NoError(t, err)
|
||||
return s
|
||||
}
|
||||
|
||||
signer, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
serialNumber, err := asn1.Marshal(1234)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
leaf := makeLeaf(signer, serialNumber)
|
||||
require.NoError(t, err)
|
||||
|
||||
fingerprint, err := keyutil.Fingerprint(signer.Public())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
require.NoError(t, err)
|
||||
|
||||
serialNumber, err := asn1.Marshal(1234)
|
||||
require.NoError(t, err)
|
||||
|
||||
leaf := makeLeaf(signer, serialNumber)
|
||||
leafWithStepManagedDeviceID := makeLeafWithStepManagedDeviceID(signer, "1234")
|
||||
|
||||
jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
require.NoError(t, err)
|
||||
|
||||
keyAuth, err := KeyAuthorization("token", jwk)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
require.NoError(t, err)
|
||||
|
||||
keyAuthSum := sha256.Sum256([]byte(keyAuth))
|
||||
sig, err := signer.Sign(rand.Reader, keyAuthSum[:], crypto.SHA256)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
require.NoError(t, err)
|
||||
|
||||
cborSig, err := cbor.Marshal(sig)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
require.NoError(t, err)
|
||||
|
||||
otherSigner, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
require.NoError(t, err)
|
||||
|
||||
otherSig, err := otherSigner.Sign(rand.Reader, keyAuthSum[:], crypto.SHA256)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
require.NoError(t, err)
|
||||
|
||||
otherCBORSig, err := cbor.Marshal(otherSig)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
require.NoError(t, err)
|
||||
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
@@ -3595,6 +3650,18 @@ func Test_doStepAttestationFormat(t *testing.T) {
|
||||
Certificate: leaf,
|
||||
Fingerprint: fingerprint,
|
||||
}, false},
|
||||
{"ok/step-managed-device-id", args{ctx, mustAttestationProvisioner(t, caRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
|
||||
Format: "step",
|
||||
AttStatement: map[string]interface{}{
|
||||
"x5c": []interface{}{leafWithStepManagedDeviceID.Raw, ca.Intermediate.Raw},
|
||||
"alg": -7,
|
||||
"sig": cborSig,
|
||||
},
|
||||
}}, &stepAttestationData{
|
||||
SerialNumber: "1234",
|
||||
Certificate: leafWithStepManagedDeviceID,
|
||||
Fingerprint: fingerprint,
|
||||
}, false},
|
||||
{"fail yubico issuer", args{ctx, mustAttestationProvisioner(t, nil), &Challenge{Token: "token"}, jwk, &attestationObject{
|
||||
Format: "step",
|
||||
AttStatement: map[string]interface{}{
|
||||
@@ -4757,6 +4824,54 @@ func Test_deviceAttest01Validate(t *testing.T) {
|
||||
caRoot := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: root.Raw})
|
||||
ctx := NewProvisionerContext(context.Background(), mustAttestationProvisioner(t, caRoot))
|
||||
|
||||
return test{
|
||||
args: args{
|
||||
ctx: ctx,
|
||||
jwk: jwk,
|
||||
ch: &Challenge{
|
||||
ID: "chID",
|
||||
AuthorizationID: "azID",
|
||||
Token: "token",
|
||||
Type: "device-attest-01",
|
||||
Status: StatusPending,
|
||||
Value: "12345678",
|
||||
},
|
||||
payload: payload,
|
||||
db: &MockDB{
|
||||
MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {
|
||||
assert.Equal(t, "azID", id)
|
||||
return &Authorization{ID: "azID"}, nil
|
||||
},
|
||||
MockUpdateAuthorization: func(ctx context.Context, az *Authorization) error {
|
||||
fingerprint, err := keyutil.Fingerprint(leaf.PublicKey)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "azID", az.ID)
|
||||
assert.Equal(t, fingerprint, az.Fingerprint)
|
||||
return nil
|
||||
},
|
||||
MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
|
||||
assert.Equal(t, "chID", updch.ID)
|
||||
assert.Equal(t, "token", updch.Token)
|
||||
assert.Equal(t, StatusValid, updch.Status)
|
||||
assert.Equal(t, ChallengeType("device-attest-01"), updch.Type)
|
||||
assert.Equal(t, "12345678", updch.Value)
|
||||
assert.Equal(t, payload, updch.Payload)
|
||||
assert.Equal(t, "step", updch.PayloadFormat)
|
||||
|
||||
return nil
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: nil,
|
||||
}
|
||||
},
|
||||
"ok/step-managed-device-id": func(t *testing.T) test {
|
||||
jwk, keyAuth := mustAccountAndKeyAuthorization(t, "token")
|
||||
payload, leaf, root := mustAttestStepManagedDeviceID(t, "nonce", keyAuth, "12345678")
|
||||
|
||||
caRoot := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: root.Raw})
|
||||
ctx := NewProvisionerContext(context.Background(), mustAttestationProvisioner(t, caRoot))
|
||||
|
||||
return test{
|
||||
args: args{
|
||||
ctx: ctx,
|
||||
|
||||
Reference in New Issue
Block a user