pax_global_header00006660000000000000000000000064131076674470014531gustar00rootroot0000000000000052 comment=f28518f20029f7ed12a8ced4fbce8c86981b4c5c go-ykpiv-1.1/000077500000000000000000000000001310766744700131375ustar00rootroot00000000000000go-ykpiv-1.1/.dockerignore000066400000000000000000000000361310766744700156120ustar00rootroot00000000000000.dockerignore .git Dockerfile go-ykpiv-1.1/.gitignore000066400000000000000000000000051310766744700151220ustar00rootroot00000000000000*swp go-ykpiv-1.1/AUTHORS000066400000000000000000000001101310766744700141770ustar00rootroot00000000000000Paul Tagliamonte Tianon Gravi go-ykpiv-1.1/Dockerfile000066400000000000000000000010131310766744700151240ustar00rootroot00000000000000# $ docker build -t paultag/go-ykpiv . # $ docker run -it --rm -v /run/pcscd/pcscd.comm:/run/pcscd/pcscd.comm paultag/go-ykpiv FROM debian:stretch-slim RUN apt-get update \ && apt-get install -y --no-install-recommends \ ca-certificates \ gcc \ git \ golang-any \ libc6-dev \ libykpiv-dev \ \ # this isn't strictly necessary, but it's a useful tool to have available yubico-piv-tool \ && rm -rf /var/lib/apt/lists/* ENV GOPATH /go WORKDIR $GOPATH/src/pault.ag/go/ykpiv COPY . . RUN go get -v -t ./... go-ykpiv-1.1/LICENSE000066400000000000000000000020741310766744700141470ustar00rootroot00000000000000Copyright (c) Paul R. Tagliamonte , 2017 Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. go-ykpiv-1.1/README.md000066400000000000000000000063041310766744700144210ustar00rootroot00000000000000go-ykpiv ======== go-ykpiv is a high level cgo wrapper around `libykpiv.so.1` that implements an idiomatic go API fit for use when applications need to communicate with a Yubikey in PIV mode. What's PIV? ----------- PIV Cards are cards defined by FIPS 201, a Federal US Government standard defining the ID cards employees use. At its core, it's a set of x509 Certificates and corresponding private keys in a configuration that is standardized accross implementations. For more details on how PIV Tokens can be used, the FICAM (Federal Identity, Credential, and Access Management) team at GSA (General Services Administration) has published some guides on GitHub under [GSA/piv-guides](https://github.com/GSA/piv-guides) How is this different than OpenSC? ---------------------------------- Most PIV tokens, Yubikeys included, can be used as a PKCS#11 device using [OpenSC](https://github.com/opensc/opensc), and Yubikeys are even capable of doing Signing and Decryption through that interface. However, some management functions are not exposed in the PKCS#11 OpenSC interface, so this library may be of use when one wants to write a new Certificate, or set PINs. Development =========== Testing ------- To run the tests, you'll need to find a Yubikey that you're willing to wipe clean, and destroy all data on it. After you've found such a key, remove all other Yubikeys from your machine. The tests will panic if the `YKPIV_YES_DESTROY_MY_KEY` environment variable is unset. Running the tests will **reset** your Yubikey a few times (once per test), and you will wind up with a key with the default PIN, PUK and Management Key. Installation ============ Debian ------ ``` sudo apt install build-essential libykpiv-dev go get pault.ag/go/ykpiv ``` Examples ======== ```go package main import ( "fmt" "pault.ag/go/ykpiv" ) func main() { yubikey, err := ykpiv.New(ykpiv.Options{ // Verbose: true, Reader: "Yubico Yubikey NEO U2F+CCID 01 00", }) if err != nil { panic(err) } defer yubikey.Close() version, err := yubikey.Version() if err != nil { panic(err) } fmt.Printf("Application version %s found.\n", version) } ``` LICENSE ======= ``` Copyright (c) Paul R. Tagliamonte , 2017 Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ``` go-ykpiv-1.1/decrypt.go000066400000000000000000000045361310766744700151500ustar00rootroot00000000000000// {{{ Copyright (c) Paul R. Tagliamonte , 2017 // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. }}} package ykpiv /* #cgo LDFLAGS: -lykpiv #cgo CFLAGS: -I/usr/include/ykpiv/ #include #include */ import "C" import ( "io" "unsafe" "crypto" "pault.ag/go/ykpiv/internal/pkcs1v15" ) // Decrypt decrypts ciphertext with the private key backing the Slot we're operating // on. This implements the crypto.Decrypter interface. // // The `rand` argument is disregarded in favor of the on-chip RNG on the Yubikey // The `opts` argument is not used at this time, but may in the future. func (s Slot) Decrypt(rand io.Reader, msg []byte, opts crypto.DecrypterOpts) ([]byte, error) { var cMessage = (*C.uchar)(C.CBytes(msg)) defer C.free(unsafe.Pointer(cMessage)) var cMessageLen = C.size_t(len(msg)) var cPlaintextLen = C.size_t(len(msg)) var cPlaintext = (*C.uchar)(C.malloc(cMessageLen)) defer C.free(unsafe.Pointer(cPlaintext)) algorithm, err := s.getAlgorithm() if err != nil { return nil, err } if err := getError(C.ykpiv_decipher_data( s.yubikey.state, cMessage, cMessageLen, cPlaintext, &cPlaintextLen, algorithm, C.uchar(s.Id.Key), ), "decipher_data"); err != nil { return nil, err } return pkcs1v15.Unpad(C.GoBytes(unsafe.Pointer(cPlaintext), C.int(cPlaintextLen))) } // vim: foldmethod=marker go-ykpiv-1.1/docs/000077500000000000000000000000001310766744700140675ustar00rootroot00000000000000go-ykpiv-1.1/docs/quirks.md000066400000000000000000000026341310766744700157340ustar00rootroot00000000000000Qurks of ykpiv ============== ykpiv.Yubikey.Slot ------------------ Slots which have private keys backing them are unable to be loaded since the Certificate will fail to be read. This could be fixed by pulling the Public Key material for that slot, but I can't find any obvious way to do so, so the code only pulls Public Key material from the Certificate in the Slot. Basically, the only way to get a Slot with a nill Certificate is to call the Generation code, where we are actually able to pull the Public Key material out, and allow for bootstrapping. The advisable workflow would be to generate the key, then sign a CSR to import the Certificate later when we have one, or to Self Sign a Certificate, then import it. ykpiv.Slot.Update ----------------- Because the Certificate is hanging off the Slot object, there is a tradeoff in terms of number of times we read the Certificate off the Key, and the ability to get updated Certificates when they are pushed in. There have been no attempts to be clever here, so it is *not* advisable to keep a `Slot` around for long-running processes, if you can avoid it. If it becomes strictly needed, at least understand your Certificate may become out of sync if another process Updates it. ykpiv.Options / ykpiv.Yubikey.ChangePIN --------------------------------------- Changing the PIN does not touch the Options entries, and attempts to Login after ChangePIN will use the old PIN. go-ykpiv-1.1/errors.go000066400000000000000000000116531310766744700150100ustar00rootroot00000000000000// {{{ Copyright (c) Paul R. Tagliamonte , 2017 // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. }}} package ykpiv /* #cgo LDFLAGS: -lykpiv #cgo CFLAGS: -I/usr/include/ykpiv/ #include */ import "C" import ( "fmt" ) // Go wrapper around the internal C ykpiv error integers. Error codes as // they exist in ykpiv.h are brought into Go, since we can add some additional // context around them, as well as implement the Error interface. type Error struct { // int representing the underlying error code as ykpiv had given us. The // exact numbers can be found in your local ykpiv.h, inside the ykpiv_rc // enum. Code int // Human readable message regarding what happened. Message string // internal marker to know where this fell out of. it's helpful to know if // this came out of ykpiv_sign_data or ykpiv_done where string } // Check to see if another Error is the same as our struct. This compares // the underlying Code integer. func (e Error) Equal(err error) bool { otherError, ok := err.(Error) if !ok { return false } return e.Code == otherError.Code } // Error interface. This will sprintf a string containing where this error // came from, the human message, and the underlying ykpiv code, to aid with // debugging. func (e Error) Error() string { return fmt.Sprintf("%s: %s (%d) - %s", e.where, e.Message, e.Code, C.GoString(C.ykpiv_strerror(C.ykpiv_rc(e.Code)))) } // Create a helpful mapping between 8 bit integers and the Error that it // belongs to. This is used to look errors up at runtime later. func createErrorLookupMap(errs ...Error) map[int]Error { ret := map[int]Error{} for _, err := range errs { ret[err.Code] = err } return ret } var ( MemoryError = Error{Code: C.YKPIV_MEMORY_ERROR, Message: "Memory Error"} PCSCError = Error{Code: C.YKPIV_PCSC_ERROR, Message: "PKCS Error"} SizeError = Error{Code: C.YKPIV_SIZE_ERROR, Message: "Size Error"} AppletError = Error{Code: C.YKPIV_APPLET_ERROR, Message: "Applet Error"} AuthenticationError = Error{Code: C.YKPIV_AUTHENTICATION_ERROR, Message: "Authentication Error"} RandomnessError = Error{Code: C.YKPIV_RANDOMNESS_ERROR, Message: "Randomness Error"} GenericError = Error{Code: C.YKPIV_GENERIC_ERROR, Message: "Generic Error"} KeyError = Error{Code: C.YKPIV_KEY_ERROR, Message: "Key Error"} ParseError = Error{Code: C.YKPIV_PARSE_ERROR, Message: "Parse Error"} WrongPIN = Error{Code: C.YKPIV_WRONG_PIN, Message: "Wrong PIN"} InvalidObject = Error{Code: C.YKPIV_INVALID_OBJECT, Message: "Invalid Object"} AlgorithmError = Error{Code: C.YKPIV_ALGORITHM_ERROR, Message: "Algorithm Error"} PINLockedError = Error{Code: C.YKPIV_PIN_LOCKED, Message: "PIN Locked"} SecurityStatusError = Error{Code: C.SW_ERR_SECURITY_STATUS, Message: "Security Status Error"} AuthBlocked = Error{Code: C.SW_ERR_AUTH_BLOCKED, Message: "Auth Blocked"} IncorrectParam = Error{Code: C.SW_ERR_INCORRECT_PARAM, Message: "Incorrect Param"} IncorrectSlot = Error{Code: C.SW_ERR_INCORRECT_SLOT, Message: "Incorrect Slot"} errorLookupMap = createErrorLookupMap(MemoryError, PCSCError, SizeError, AppletError, AuthenticationError, RandomnessError, GenericError, KeyError, ParseError, WrongPIN, InvalidObject, AlgorithmError, PINLockedError) swErrorLookupMap = createErrorLookupMap(SecurityStatusError, AuthBlocked, IncorrectParam, IncorrectSlot) ) // Take a ykpiv_rc return code and turn it into a ykpiv.Error. func getError(whoops C.ykpiv_rc, name string) error { if err, ok := errorLookupMap[int(whoops)]; ok { err.where = fmt.Sprintf("ykpiv ykpiv_%s", name) return err } return nil } // Take a ykpiv_rc return code and turn it into a ykpiv.Error. func getSWError(whoops int, name string) error { if err, ok := swErrorLookupMap[int(whoops)]; ok { err.where = fmt.Sprintf("ykpiv sw ykpiv_%s", name) return err } return nil } // vim: foldmethod=marker go-ykpiv-1.1/generate.go000066400000000000000000000101341310766744700152570ustar00rootroot00000000000000// {{{ Copyright (c) Paul R. Tagliamonte , 2017 // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. }}} package ykpiv /* #cgo LDFLAGS: -lykpiv #cgo CFLAGS: -I/usr/include/ykpiv/ #include #include */ import "C" import ( "fmt" "math/big" "crypto" "crypto/rsa" "pault.ag/go/ykpiv/internal/bytearray" ) var ( // Tell the Yubikey to generate an asymetric key (like RSA or RCC) ykpivInsGenerateAsymetric byte = 0x47 ) // Decode a DER encoded list of byte arrays into an rsa.PublicKey. func decodeYubikeyRSAPublicKey(der []byte) (*rsa.PublicKey, error) { byteArray, err := bytearray.DERDecode(der) if err != nil { return nil, err } if len(byteArray) != 2 { return nil, fmt.Errorf("ykpiv: decodeYubikeyRSAPublicKey: Byte Array isn't length 2") } n := byteArray[0] if n.Tag != 1 { return nil, fmt.Errorf("ykpiv: decodeYubikeyRSAPublicKey: I'm confused about n: %x", n.Tag) } pubN := big.NewInt(0) pubN.SetBytes(n.Bytes) e := byteArray[1] if e.Tag != 2 { return nil, fmt.Errorf("ykpiv: decodeYubikeyRSAPublicKey: I'm confused about e: %x", e.Tag) } pubE := big.NewInt(0) pubE.SetBytes(e.Bytes) pubKey := rsa.PublicKey{ N: pubN, E: int(pubE.Int64()), } return &pubKey, nil } // Generate an RSA Keypair in slot `id` (using a modulus size of `bits`), // and construct a Certificate-less Slot. This Slot can not be recovered // later, so it should be used to sign a CSR or Self-Signed Certificate // before we lose the key material. func (y Yubikey) GenerateRSA(id SlotId, bits int) (*Slot, error) { pubKey, err := y.generateRSAKey(id, bits) if err != nil { return nil, err } return &Slot{yubikey: y, Id: id, PublicKey: pubKey}, nil } // Generate an RSA public key on the Yubikey, parse the output and return // a crypto.PublicKey. This will create the key in slot `slot`, with a // modulus size of `bits`. func (y Yubikey) generateRSAKey(slot SlotId, bits int) (crypto.PublicKey, error) { var algorithm byte switch bits { case 1024: algorithm = C.YKPIV_ALGO_RSA1024 case 2048: algorithm = C.YKPIV_ALGO_RSA2048 default: return nil, fmt.Errorf("ykpiv: GenerateRSA: Unknown bit size: %d", bits) } der, err := y.generateKey(slot, algorithm) if err != nil { return nil, err } return decodeYubikeyRSAPublicKey(der) } // This is a low-level binding into the underlying instruction to actually // generate a new asymetric key on the Yubikey. This will create a key of // type `algorithm` (something like C.YKPIV_ALGO_RSA2048) in slot `slot`. // // This will return the raw bytes from the actual Yubikey itself back to // the caller to appropriately parse the output. In the case of RSA keys, // this is a DER encoded series of DER encoded byte arrays for N and E. func (y Yubikey) generateKey(slot SlotId, algorithm byte) ([]byte, error) { sw, data, err := y.transferData( []byte{0x00, ykpivInsGenerateAsymetric, 0x00, byte(slot.Key)}, []byte{0xAC, 3, C.YKPIV_ALGO_TAG, 1, algorithm}, 1024, ) if err != nil { return nil, err } err = getSWError(sw, "transfer_data") if err != nil { return nil, err } return data, nil } // vim: foldmethod=marker go-ykpiv-1.1/internal/000077500000000000000000000000001310766744700147535ustar00rootroot00000000000000go-ykpiv-1.1/internal/bytearray/000077500000000000000000000000001310766744700167555ustar00rootroot00000000000000go-ykpiv-1.1/internal/bytearray/bytearray.go000066400000000000000000000043331310766744700213110ustar00rootroot00000000000000// {{{ Copyright (c) Paul R. Tagliamonte , 2017 // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. }}} package bytearray import ( "encoding/asn1" ) // Decode will unpack a byte array of DER encoded byte arrays into // asn1.RawValue structs. func Decode(bytes []byte) ([]asn1.RawValue, error) { ret := []asn1.RawValue{} for { rawData := asn1.RawValue{} rest, err := asn1.Unmarshal(bytes, &rawData) if err != nil { return nil, err } ret = append(ret, rawData) if len(rest) == 0 { break } bytes = rest } return ret, nil } // This will DER unpack a byte array, and Decode the nested Byte array // that sits underneath it. func DERDecode(bytes []byte) ([]asn1.RawValue, error) { rawData := asn1.RawValue{} if _, err := asn1.Unmarshal(bytes, &rawData); err != nil { return nil, err } return Decode(rawData.Bytes) } // Take a list of asn1.RawValue structs, Marshal them, and push the combined // array into a byte array to drop out. func Encode(values []asn1.RawValue) ([]byte, error) { ret := []byte{} for _, value := range values { bytes, err := asn1.Marshal(value) if err != nil { return nil, err } ret = append(ret, bytes...) } return ret, nil } // vim: foldmethod=marker go-ykpiv-1.1/internal/pkcs1v15/000077500000000000000000000000001310766744700163305ustar00rootroot00000000000000go-ykpiv-1.1/internal/pkcs1v15/pad.go000066400000000000000000000045751310766744700174360ustar00rootroot00000000000000// {{{ Copyright (c) Paul R. Tagliamonte , 2017 // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. }}} package pkcs1v15 import ( "fmt" ) // PKCS#1 1.5 defines a method to pad data passed into a signing operation // which is (basically) to set some bits at the lower indexes, then a bunch of // 0xFF, finally, a 0x00, then the data until the end of the block. // Pad a message to padLen bytes according to PKCS#1 v1.5 rules func Pad(message []byte, padLen int) []byte { padding := make([]byte, (padLen - 3 - len(message))) for i := 0; i < len(padding); i++ { padding[i] = 0xFF } return expandBytes([]byte{0x00, 0x01}, padding, []byte{0x00}, message) } // Unpad a message according to PKCS#1 v1.5 rules func Unpad(message []byte) ([]byte, error) { for i := 2; i < len(message); i++ { if message[i] == 0x00 { return message[i+1:], nil } // if message[i] != 0xFF { // return nil, fmt.Errorf("ykpiv: pkcs1v15: Invalid padding byte: %x", message[i]) // } } return nil, fmt.Errorf("ykpiv: pkcs1v15: Input does not appear to be in PKCS#1 v 1.5 padded format") } // Take some byte arrays, and return the concatenation of all of those byte // arrays. It's basically like append, but for byte arrays, not bytes. func expandBytes(els ...[]byte) []byte { out := []byte{} for _, el := range els { out = append(out, el...) } return out } // vim: foldmethod=marker go-ykpiv-1.1/pivman.go000066400000000000000000000073271310766744700147710ustar00rootroot00000000000000// {{{ Copyright (c) Paul R. Tagliamonte , 2017 // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. }}} package ykpiv /* #cgo LDFLAGS: -lykpiv #cgo CFLAGS: -I/usr/include/ykpiv/ #include #include */ import "C" import ( "crypto" "golang.org/x/crypto/pbkdf2" "pault.ag/go/ykpiv/internal/bytearray" ) var ( pivmanObjData = 0x5FFF00 /* pivman's source defines this as 0x80, but since we're using an actual * der decoder, we'll see the tag value, which would just be 1 */ pivmanTagFlags1 = 0x01 pivmanTagSalt = 0x02 pivmanTagTimestamp = 0x03 pivmanTagFlags1PUKBlocked = 0x01 ) // Get the salt off the Yubikey PIV token, which is stored in a DER encoded // array of arrays. This salt is a couple of bytes of calming entropy. func (y Yubikey) getSalt() ([]byte, error) { attributes, err := y.getPIVMANAttributes() if err != nil { return nil, err } return attributes[pivmanTagSalt], nil } // Compute the PIVMAN Management Key using 10000 rounds of PBKDF2 SHA1 // utilizing the salt off the Yubikey to derive the 3DES management key. func (y Yubikey) deriveManagementKey() ([]byte, error) { // Description of the Management key derivation can be found on the // Yubikey website: // https://developers.yubico.com/yubikey-piv-manager/PIN_and_Management_Key.html // // Technical description of Key derivation from PIN // // When choosing to use a Management Key derived from the PIN, the following takes place: // // A random 8-byte SALT value is generated and stored on the YubiKey. // // The derived Management Key is calculated as PBKDF2(PIN, SALT, 24, 10000). // // The PBKDF2 function (described in RFC 2898) is run using the PIN // (encoded using UTF-8) as the password, for 10000 rounds, to produce a 24 // byte key, which is used as the management key. Whenever the user changes // the PIN this process is repeated, using a new SALT and the new PIN. pin, err := y.options.GetPIN() if err != nil { return nil, err } salt, err := y.getSalt() if err != nil { return nil, err } return pbkdf2.Key([]byte(pin), salt, 10000, 24, crypto.SHA1.New), nil } // Return a mapping of pivmanTags -> byte arrays. The exact semantics // of this byte array is defined entirely by the tag, and should be treated // as semantically opaque to the user, unless specific parsing code is in place. func (y Yubikey) getPIVMANAttributes() (map[int][]byte, error) { attributes := map[int][]byte{} bytes, err := y.GetObject(pivmanObjData) if err != nil { return nil, err } byteArray, err := bytearray.DERDecode(bytes) if err != nil { return nil, err } for _, rawValue := range byteArray { attributes[rawValue.Tag] = rawValue.Bytes } return attributes, nil } // vim: foldmethod=marker go-ykpiv-1.1/sign.go000066400000000000000000000075041310766744700144340ustar00rootroot00000000000000// {{{ Copyright (c) Paul R. Tagliamonte , 2017 // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. }}} package ykpiv /* #cgo LDFLAGS: -lykpiv #cgo CFLAGS: -I/usr/include/ykpiv/ #include #include */ import "C" import ( "fmt" "io" "unsafe" "crypto" "pault.ag/go/ykpiv/internal/pkcs1v15" ) // It's never a real party until you import both `unsafe`, *and* `crypto`. // PKCS#1 9.2.1 defines a method to push the hash algorithm used into the // digest before the signature. More exactly, we prepend some ASN.1 with // conatins the Obejct ID for the hash algorithm used. Since we know a lot // about the digest and the OID, we can just prefix the digest with some ASN.1 // fresh off the CPU var hashOIDs = map[crypto.Hash][]byte{ crypto.SHA224: {0x30, 0x2d, 0x30, 0x0d, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x04, 0x05, 0x00, 0x04, 0x1c}, crypto.SHA256: {0x30, 0x31, 0x30, 0x0d, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x01, 0x05, 0x00, 0x04, 0x20}, crypto.SHA384: {0x30, 0x41, 0x30, 0x0d, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x02, 0x05, 0x00, 0x04, 0x30}, crypto.SHA512: {0x30, 0x51, 0x30, 0x0d, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x03, 0x05, 0x00, 0x04, 0x40}, } // Sign implements the crypto.Signer.Sign interface. // // Unlike other Sign implementations, `rand` will be completely discarded in // favor of the on-chip RNG. // // The output will be a PKCS#1 v1.5 signature over the digest. func (s Slot) Sign(_ io.Reader, digest []byte, opts crypto.SignerOpts) ([]byte, error) { hash := opts.HashFunc() if len(digest) != hash.Size() { return nil, fmt.Errorf("ykpiv: Sign: Digest length doesn't match passed crypto algorithm") } prefix, ok := hashOIDs[hash] if !ok { return nil, fmt.Errorf("ykpiv: Sign: Unsupported algorithm") } digest = append(prefix, digest...) algorithm, err := s.getAlgorithm() if err != nil { return nil, err } var computedDigest []byte switch algorithm { case C.YKPIV_ALGO_RSA1024: computedDigest = pkcs1v15.Pad(digest, 128) case C.YKPIV_ALGO_RSA2048: computedDigest = pkcs1v15.Pad(digest, 256) default: return nil, fmt.Errorf("ykpiv: Sign: Can't preform padding for signature, unknown algorithm") } var cDigestLen = C.size_t(len(computedDigest)) var cDigest = (*C.uchar)(C.CBytes(computedDigest)) defer C.free(unsafe.Pointer(cDigest)) var cSignatureLen = C.size_t(1024) var cSignature = (*C.uchar)(C.malloc(cSignatureLen)) defer C.free(unsafe.Pointer(cSignature)) if err := getError(C.ykpiv_sign_data( s.yubikey.state, cDigest, cDigestLen, cSignature, &cSignatureLen, algorithm, C.uchar(s.Id.Key), ), "sign_data"); err != nil { return nil, err } return C.GoBytes(unsafe.Pointer(cSignature), C.int(cSignatureLen)), nil } // vim: foldmethod=marker go-ykpiv-1.1/slot.go000066400000000000000000000140621310766744700144520ustar00rootroot00000000000000// {{{ Copyright (c) Paul R. Tagliamonte , 2017 // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. }}} package ykpiv /* #cgo LDFLAGS: -lykpiv #cgo CFLAGS: -I/usr/include/ykpiv/ #include #include */ import "C" import ( "fmt" "crypto" "crypto/rsa" "crypto/x509" ) // SlotId encapsulates the Identifiers required to preform key operations // on the Yubikey. The identifier most people would know (if this is a thing // that people do who don't write PIV aware applications) would be the `Key` // Id, something like 0x9A. type SlotId struct { Certificate int32 Key int32 Name string } // Return a human readable string mostly useful for debugging which Slot you // might have your hands on. Since most people (for some value of "most") // would want the Key, this only includes that. func (s SlotId) String() string { return fmt.Sprintf("%s (%x)", s.Name, s.Key) } // More information regarding the basic PIV slots can be founnd at the // FICAM piv-gude: https://piv.idmanagement.gov/elements/ var ( // PIV Authentication, which is a certificate and key pair and can be used // to verify that the PIV credential was issued by an authorized entity, // has not expired, has not been revoked, and holder of the credential // (YOU) is the same individual it was issued to. Authentication SlotId = SlotId{ Certificate: C.YKPIV_OBJ_AUTHENTICATION, Key: C.YKPIV_KEY_AUTHENTICATION, Name: "Authentication", } // Digital Signature, which is a certificate and key pair allows the YOU to // digitally sign a document or email, providing both integrity and // non-repudiation. Signature SlotId = SlotId{ Certificate: C.YKPIV_OBJ_SIGNATURE, Key: C.YKPIV_KEY_SIGNATURE, Name: "Digital Signature", } // Card Authentication, which is a certificate and key pair that can be // used to verify that the PIV credential was issued by an authorized // entity, has not expired, and has not been revoked. CardAuthentication SlotId = SlotId{ Certificate: C.YKPIV_OBJ_CARD_AUTH, Key: C.YKPIV_KEY_CARDAUTH, Name: "Card Authentication", } KeyManagement SlotId = SlotId{ Certificate: C.YKPIV_OBJ_KEY_MANAGEMENT, Key: C.YKPIV_KEY_KEYMGM, Name: "Key Management", } ) // Slot abstracts a public key, private key, and x509 Certificate stored // on the PIV device. // // Internally, this keeps track of the Yubikey this came from, the underlying // object identifiers for the Certificate and Key we care about, as well as // other bits and bobs of state. type Slot struct { yubikey Yubikey Id SlotId PublicKey crypto.PublicKey Certificate *x509.Certificate } // Get the PIV Authentication Slot off the Yubikey. This is identical to // invoking `yubikey.Slot(ykpiv.Authentication)`. func (y Yubikey) Authentication() (*Slot, error) { return y.Slot(Authentication) } // Get the Digital Signature Slot off the Yubikey. This is identical to // invoking `yubikey.Slot(ykpiv.Signature)` func (y Yubikey) Signature() (*Slot, error) { return y.Slot(Signature) } // Get the PIV Card Authentication Slot off the Yubikey. This is identical to // invoking `yubikey.Slot(ykpiv.CardAuthentication)` func (y Yubikey) CardAuthentication() (*Slot, error) { return y.Slot(CardAuthentication) } // Get the PIV Key Management Slot off the Yubikey. This is identical to // invoking `yubikey.Slot(ykpiv.KeyManagement)` func (y Yubikey) KeyManagement() (*Slot, error) { return y.Slot(KeyManagement) } // Get a Slot off of the Yubikey by the SlotId. // // This will trigger an attempt to get (and parse) the x509 Certificate // for this slot. Only slots with an x509 Certificate can be used. func (y Yubikey) Slot(id SlotId) (*Slot, error) { /* Right, let's see what we can do here */ slot := Slot{yubikey: y, Id: id} certificate, err := slot.GetCertificate() if err != nil { return nil, err } slot.Certificate = certificate slot.PublicKey = certificate.PublicKey return &slot, nil } // Return the crypto.PublicKey that we know corresponds to the Certificate // we have on hand. func (s Slot) Public() crypto.PublicKey { return s.PublicKey } // Get the Yubikey C.YKPIV_ALGO_* uchar for the key material backing the // slot. func (y Slot) getAlgorithm() (C.uchar, error) { pubKey := y.PublicKey switch pubKey.(type) { case *rsa.PublicKey: rsaPub := pubKey.(*rsa.PublicKey) switch rsaPub.N.BitLen() { case 1024: return C.YKPIV_ALGO_RSA1024, nil case 2048: return C.YKPIV_ALGO_RSA2048, nil default: return C.uchar(0), fmt.Errorf("ykpiv: getAlgorithm: Unknown RSA Modulus size") } default: return C.uchar(0), fmt.Errorf("ykpiv: getAlgorithm: Unknown public key algorithm") } } // Get the x509.Certificate stored in the PIV Slot off the chip func (y Slot) GetCertificate() (*x509.Certificate, error) { return y.yubikey.GetCertificate(y.Id) } // Write the x509 Certificate to the Yubikey. func (y *Slot) Update(cert x509.Certificate) error { if err := y.yubikey.SaveCertificate(y.Id, cert); err != nil { return err } y.Certificate = &cert return nil } // vim: foldmethod=marker go-ykpiv-1.1/tls.go000066400000000000000000000027301310766744700142720ustar00rootroot00000000000000// {{{ Copyright (c) Paul R. Tagliamonte , 2017 // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. }}} package ykpiv import ( "crypto/tls" ) // Create a tls.Certificate fit for use in crypto/tls applications, // such as net/http, or grpc. func (slot Slot) TLSCertificate() tls.Certificate { return tls.Certificate{ Certificate: [][]byte{slot.Certificate.Raw}, PrivateKey: slot, Leaf: slot.Certificate, } } // vim: foldmethod=marker go-ykpiv-1.1/ykpiv.go000066400000000000000000000333311310766744700146330ustar00rootroot00000000000000// {{{ Copyright (c) Paul R. Tagliamonte , 2017 // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. }}} package ykpiv // XXX: -Wl,--allow-multiple-definition is needed because the test suite fails // when I build it. For now this will keep it quiet :\ /* #cgo LDFLAGS: -lykpiv -Wl,--allow-multiple-definition #cgo CFLAGS: -I/usr/include/ykpiv/ #include #include */ import "C" import ( "bytes" "crypto/x509" "encoding/asn1" "fmt" "unsafe" "pault.ag/go/ykpiv/internal/bytearray" ) // Configuration for initialization of the Yubikey, as well as options that // may be used during runtime. type Options struct { // When true, this will cause the underlying ykpiv library to emit additional // information to stderr. This can be helpful when debugging why something // isn't working as expected. Verbose bool // String to be used when searching for a Yubikey. The comparison // will be against the output you can observe from // `yubico-piv-tool -a list-readers`. Reader string // PIN is the pin that will be used when logging in PIN *string // PUK is the PUK to be used when logging in PUK *string // ManagementKey is the Management Key to be used for key operations ManagementKey []byte // Flag to let ykpiv know if this PIV token has a ManagementKey that was // set by pivman, which is a PBKDF2 SHA1 key derived with a salt held on // chip in the internal pivman data. // // If this is `true`, ManagementKey will be ignored in favor of deriving // the key from the PIN. ManagementKeyIsPIN bool } // Get the Management Key. // // On some configurations, users have set the Management Key to a PBKDF2 // SHA1 key derived from the PIN, so this may return one of two things: // // If `ManagementKeyIsPIN` is false, the `ManagementKey` byte array // will be returned. // // If `ManagementKeyIsPIN` is true, the `PIN` will be used, in conjunction // with a salt held within the PIVMON object address to compute the // ManagementKey. If PIN is nil, this will result in an error being returned. func (o Options) GetManagementKey(y Yubikey) ([]byte, error) { key := o.ManagementKey if o.ManagementKeyIsPIN { var err error key, err = y.deriveManagementKey() if err != nil { return nil, err } } if len(key) == 0 { return nil, fmt.Errorf("ykpiv: GetManagementKey: ManagementKey is empty!") } return key, nil } // Get the user defined PUK. This will return an error if PUK is nil. func (o Options) GetPUK() (string, error) { if o.PUK == nil { return "", fmt.Errorf("ykpiv: GetPUK: No PUK set in Options") } return *(o.PUK), nil } // Get the user defined PIN. This will return an error if PUK is nil. func (o Options) GetPIN() (string, error) { if o.PIN == nil { return "", fmt.Errorf("ykpiv: GetPIN: No PIN set in Options") } return *(o.PIN), nil } // Encapsulation of the ykpiv internal state object, and the configuration // in new. This needs to be initalized through `ykpiv.New` to ensure the // internal state is brought up correctly. // // This object represents a single physical yubikey that we've connected to. // This object provides a number of helper functions hanging off the struct // to avoid keeping and passing the internal ykpiv state object in C. // // `.Close()` must be called, or this will leak memory. type Yubikey struct { state *C.ykpiv_state options Options } // Close the Yubikey object, and preform any finization needed to avoid leaking // memory or holding locks. func (y Yubikey) Close() error { if err := getError(C.ykpiv_disconnect(y.state), "disconnect"); err != nil { return err } return getError(C.ykpiv_done(y.state), "done") // calling ykpiv_done will free the underlying ykpiv_state. Doing a C.free // here will result in a double-free, but thanks for noticing and keeping // memory tidy! return nil } // Write the raw bytes out of a slot stored on the Yubikey. Callers to this // function should only do so if they understand exactly what data they're // writnig, what the data should look like, and avoid rebuilding existing // interfaces if at all possible. // // The related method, GetObject, can be used to read data later. // Care must be taken to ensure the `id` is *not* being used by // any other applications. func (y Yubikey) SaveObject(id int32, data []byte) error { cData := (*C.uchar)(C.CBytes(data)) cDataLen := C.size_t(len(data)) defer C.free(unsafe.Pointer(cData)) return getError(C.ykpiv_save_object( y.state, C.int(id), cData, cDataLen, ), "save_object") } // Get the raw bytes out of a slot stored on the Yubikey. Callers to this // function should only do so if they understand exactly what data they're // reading, what the data should look like, and avoid rebuilding existing // interfaces if at all possible. // // The related method, SaveObject, can be used to write data to be read back // later. Care must be taken to ensure the `id` is *not* being used by // any other applications. func (y Yubikey) GetObject(id int) ([]byte, error) { var cDataLen C.ulong = 4096 var cData *C.uchar = (*C.uchar)(C.malloc(4096)) defer C.free(unsafe.Pointer(cData)) if err := getError(C.ykpiv_fetch_object(y.state, C.int(id), cData, &cDataLen), "fetch_object"); err != nil { return nil, err } return C.GoBytes(unsafe.Pointer(cData), C.int(cDataLen)), nil } // Return the ykpiv application version. This is expected to be in the format of // '1.2.3', but is up to the underlying ykpiv application code. func (y Yubikey) Version() ([]byte, error) { var cVersionLen C.size_t = C.size_t(7) var cVersion *C.char = (*C.char)(C.malloc(cVersionLen)) defer C.free(unsafe.Pointer(cVersion)) if err := getError(C.ykpiv_get_version(y.state, cVersion, cVersionLen), "get_version"); err != nil { return nil, err } return C.GoBytes(unsafe.Pointer(cVersion), C.int(cVersionLen)), nil } func (y Yubikey) verify(cPin *C.char) (int, error) { tries := C.int(0) err := getError(C.ykpiv_verify(y.state, cPin, &tries), "verify") if cPin == nil && WrongPIN.Equal(err) { return int(tries), nil } if err != nil { return -1, err } return int(tries), nil } // PIN Retries func (y Yubikey) PINRetries() (int, error) { return y.verify(nil) } // Log into the Yubikey using the user PIN. func (y Yubikey) Login() error { pin, err := y.options.GetPIN() if err != nil { return err } cPin := (*C.char)(C.CString(pin)) defer C.free(unsafe.Pointer(cPin)) _, err = y.verify(cPin) return err } // Using the PUK, unblock the PIN, resetting the retry counter. func (y Yubikey) UnblockPIN(newPin string) error { tries := C.int(0) puk, err := y.options.GetPUK() if err != nil { return err } cPuk := (*C.char)(C.CString(puk)) cPukLen := C.size_t(len(puk)) defer C.free(unsafe.Pointer(cPuk)) cPin := (*C.char)(C.CString(newPin)) cPinLen := C.size_t(len(newPin)) defer C.free(unsafe.Pointer(cPin)) return getError(C.ykpiv_unblock_pin( y.state, cPuk, cPukLen, cPin, cPinLen, &tries, ), "change_puk") } // Change the PUK. func (y Yubikey) ChangePUK(newPuk string) error { if y.options.ManagementKeyIsPIN { return fmt.Errorf("ykpiv: ChangePIN: Please change your PUK through pivman") } tries := C.int(0) puk, err := y.options.GetPUK() if err != nil { return err } cOldPuk := (*C.char)(C.CString(puk)) cOldPukLen := C.size_t(len(puk)) defer C.free(unsafe.Pointer(cOldPuk)) cNewPuk := (*C.char)(C.CString(newPuk)) cNewPukLen := C.size_t(len(newPuk)) defer C.free(unsafe.Pointer(cNewPuk)) return getError(C.ykpiv_change_puk( y.state, cOldPuk, cOldPukLen, cNewPuk, cNewPukLen, &tries, ), "change_puk") } // Set the Yubikey Management Key. The Management key is a 24 byte // key that's used as a 3DES key internally to preform key operations, // such as Certificate import, or keypair generation. func (y Yubikey) SetMGMKey(key []byte) error { if y.options.ManagementKeyIsPIN { return fmt.Errorf("ykpiv: ChangePIN: Please change your Management Key through pivman") } cMgmKey := (*C.uchar)(C.CBytes(key)) defer C.free(unsafe.Pointer(cMgmKey)) return getError(C.ykpiv_set_mgmkey(y.state, cMgmKey), "set_mgmkey") } // Change your PIN on the Yubikey from the oldPin to the newPin. func (y Yubikey) ChangePIN(oldPin, newPin string) error { if y.options.ManagementKeyIsPIN { return fmt.Errorf("ykpiv: ChangePIN: Please change your PIN through pivman") } tries := C.int(0) cOldPin := (*C.char)(C.CString(oldPin)) cOldPinLen := C.size_t(len(oldPin)) defer C.free(unsafe.Pointer(cOldPin)) cNewPin := (*C.char)(C.CString(newPin)) cNewPinLen := C.size_t(len(newPin)) defer C.free(unsafe.Pointer(cNewPin)) return getError(C.ykpiv_change_pin( y.state, cOldPin, cOldPinLen, cNewPin, cNewPinLen, &tries, ), "change_pin") } // Authenticate to the Yubikey using the Management Key. This key is a 24 byte // key that's used as a 3DES key internally to write new Certificates, or // create a new keypair. func (y Yubikey) Authenticate() error { managementKey, err := y.options.GetManagementKey(y) if err != nil { return err } cKey := (*C.uchar)(C.CBytes(managementKey)) defer C.free(unsafe.Pointer(cKey)) return getError(C.ykpiv_authenticate(y.state, cKey), "authenticate") } // sw, data, error func (y Yubikey) transferData( template []byte, input []byte, maxReturnSize int, ) (int, []byte, error) { sw := C.int(0) cInputLen := C.long(len(input)) cInput := (*C.uchar)(C.CBytes(input)) defer C.free(unsafe.Pointer(cInput)) cDataLen := C.ulong(maxReturnSize) cData := (*C.uchar)(C.malloc(C.size_t(cDataLen))) defer C.free(unsafe.Pointer(cData)) cTemplate := (*C.uchar)(C.CBytes(template)) defer C.free(unsafe.Pointer(cTemplate)) if err := getError(C.ykpiv_transfer_data( y.state, cTemplate, cInput, cInputLen, cData, &cDataLen, &sw, ), "transfer_data"); err != nil { return 0, nil, err } return int(sw), C.GoBytes(unsafe.Pointer(cData), C.int(cDataLen)), nil } // Reset the Yubikey. // // This can only be done if both the PIN and PUK have been blocked, and will // wipe all data on the Key. This includes all Certificates, public and private // key material. func (y Yubikey) Reset() error { template := []byte{0, C.YKPIV_INS_RESET, 0, 0} sw, _, err := y.transferData(template, nil, 128) if err != nil { return err } return getSWError(sw, "transfer_data") } // Write the x509 Certificate to the Yubikey. func (y Yubikey) SaveCertificate(slotId SlotId, cert x509.Certificate) error { certDer, err := bytearray.Encode([]asn1.RawValue{ asn1.RawValue{Tag: 0x10, IsCompound: true, Class: 0x01, Bytes: cert.Raw}, asn1.RawValue{Tag: 0x11, IsCompound: true, Class: 0x01, Bytes: []byte{0x00}}, asn1.RawValue{Tag: 0x1E, IsCompound: true, Class: 0x03, Bytes: []byte{}}, }) if err != nil { return err } return y.SaveObject(slotId.Certificate, certDer) } func (y Yubikey) GetCertificate(slotId SlotId) (*x509.Certificate, error) { bytes, err := y.GetObject(int(slotId.Certificate)) if err != nil { return nil, err } objects, err := bytearray.Decode(bytes) if err != nil { return nil, err } if len(objects) != 3 { return nil, fmt.Errorf("ykpiv: GetCertificate: We expected two der byte arrays from the key") } return x509.ParseCertificate(objects[0].Bytes) } // Create a new Yubikey. // // This will use the options in the given `ykpiv.Options` struct to // find the correct Yubikey, initialize the underlying state, and ensure // the right bits are set. func New(opts Options) (*Yubikey, error) { yubikey := Yubikey{ state: &C.ykpiv_state{}, options: opts, } verbosity := 0 if opts.Verbose { verbosity = 1 } if err := getError(C.ykpiv_init(&yubikey.state, C.int(verbosity)), "init"); err != nil { return nil, err } something := C.CString(opts.Reader) defer C.free(unsafe.Pointer(something)) if err := getError(C.ykpiv_connect(yubikey.state, something), "connect"); err != nil { return nil, err } return &yubikey, nil } // Get a list of strings that the ykpiv library has identified as unique ways // to fetch every reader attached to the system. This can be handy to define a // "Reader" argument in ykpiv.Options, and may include things ykpiv can't talk // to. func Readers() ([]string, error) { state := &C.ykpiv_state{} if err := getError(C.ykpiv_init(&state, C.int(0)), "init"); err != nil { return nil, err } var cReadersLen = C.size_t(2048) var cReaders *C.char = (*C.char)(C.malloc(cReadersLen)) defer C.free(unsafe.Pointer(cReaders)) if err := getError(C.ykpiv_list_readers(state, cReaders, &cReadersLen), "list_readers"); err != nil { return nil, err } readerBytes := C.GoBytes(unsafe.Pointer(cReaders), C.int(cReadersLen)) readers := []string{} for _, reader := range bytes.Split(readerBytes, []byte{0x00}) { if len(reader) == 0 { continue } readers = append(readers, string(reader)) } if err := getError(C.ykpiv_done(state), "done"); err != nil { return nil, err } return readers, nil } // vim: foldmethod=marker go-ykpiv-1.1/ykpiv_test.go000066400000000000000000000160541310766744700156750ustar00rootroot00000000000000/* {{{ Copyright (c) Paul R. Tagliamonte , 2017 * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. }}} */ package ykpiv_test import ( "bytes" "fmt" "io" "log" "os" "testing" "time" "math/big" "crypto/rand" "crypto/rsa" "crypto/x509" "crypto/x509/pkix" "pault.ag/go/ykpiv" ) func isok(t *testing.T, err error) { if err != nil && err != io.EOF { log.Printf("Error! Error is not nil! %s\n", err) t.FailNow() } } func notok(t *testing.T, err error) { if err == nil { log.Printf("Error! Error is nil!\n") t.FailNow() } } func assert(t *testing.T, expr bool, what string) { if !expr { log.Printf("Assertion failed: %s", what) t.FailNow() } } func isDestructive() { if os.Getenv("YKPIV_YES_DESTROY_MY_KEY") == "" { panic("export YKPIV_YES_DESTROY_MY_KEY=true # if you want to test this code on a Key") } if err := wipeYubikey(); err != nil { panic(err) } } func getYubikey(PIN, PUK string) (*ykpiv.Yubikey, func() error, error) { yk, err := ykpiv.New(ykpiv.Options{ Reader: yubikeyReaderName, PIN: &PIN, PUK: &PUK, ManagementKeyIsPIN: false, ManagementKey: []byte{ 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, }, }) if err != nil { return nil, nil, err } return yk, yk.Close, nil } func wipeYubikey() error { yubikey, closer, err := getYubikey("654321", "87654321") if err != nil { return err } defer closer() retries, err := yubikey.PINRetries() if err != nil { return err } for i := 0; i < retries; i++ { yubikey.Login() } retries, _ = yubikey.PINRetries() if retries != 0 { return fmt.Errorf("Error wiping Yubikey") } yubikey.ChangePUK("87654321") yubikey.ChangePUK("87654321") yubikey.ChangePUK("87654321") return yubikey.Reset() } var yubikeyReaderName = "Yubikey" var defaultPIN = "123456" var defaultPUK = "12345678" var allSlots = []ykpiv.SlotId{ ykpiv.Authentication, ykpiv.Signature, ykpiv.KeyManagement, ykpiv.CardAuthentication, } func TestReader(t *testing.T) { readers, err := ykpiv.Readers() isok(t, err) assert(t, len(readers) != 0, "No readers found") } func certificateTemplate() x509.Certificate { notBefore := time.Now() notAfter := notBefore.Add(time.Minute * 1) return x509.Certificate{ SerialNumber: big.NewInt(0x1337), Subject: pkix.Name{ CommonName: "p̶͕͉̟ͅḁ̲̳̕u̪̬̯̗͎͡l̷͍͎̤̠t̥̗͞ag", Organization: []string{"go-ykpiv"}, }, NotBefore: notBefore, NotAfter: notAfter, KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageKeyAgreement, BasicConstraintsValid: true, } } func TestUpdate(t *testing.T) { isDestructive() yubikey, closer, err := getYubikey(defaultPIN, defaultPUK) isok(t, err) defer closer() isok(t, yubikey.Login()) isok(t, yubikey.Authenticate()) slot, err := yubikey.GenerateRSA(ykpiv.Authentication, 1024) isok(t, err) template := certificateTemplate() derCertificate, err := x509.CreateCertificate(rand.Reader, &template, &template, slot.PublicKey, slot) isok(t, err) certificate, err := x509.ParseCertificate(derCertificate) isok(t, err) isok(t, slot.Update(*certificate)) authentication, err := yubikey.Authentication() isok(t, err) assert( t, authentication.Certificate.Subject.CommonName == template.Subject.CommonName, "Common Name is wrong", ) // Now, let's assert it's not what we're going to check next. assert( t, authentication.Certificate.Subject.CommonName != "paultag", "Common Name is wrong", ) template.Subject.CommonName = "paultag" derCertificate, err = x509.CreateCertificate(rand.Reader, &template, &template, slot.PublicKey, slot) isok(t, err) certificate, err = x509.ParseCertificate(derCertificate) isok(t, err) isok(t, slot.Update(*certificate)) authentication, err = yubikey.Authentication() isok(t, err) assert( t, authentication.Certificate.Subject.CommonName == "paultag", "Common Name is wrong", ) } func TestGenerateRSAEncryption(t *testing.T) { isDestructive() yubikey, closer, err := getYubikey(defaultPIN, defaultPUK) isok(t, err) defer closer() isok(t, yubikey.Login()) isok(t, yubikey.Authenticate()) slot, err := yubikey.GenerateRSA(ykpiv.Authentication, 1024) isok(t, err) assert(t, slot.PublicKey.(*rsa.PublicKey).N.BitLen() == 1024, "BitLen is wrong") plaintext := []byte("Well ain't this dandy") ciphertext, err := rsa.EncryptPKCS1v15(rand.Reader, slot.PublicKey.(*rsa.PublicKey), plaintext) isok(t, err) computedPlaintext, err := slot.Decrypt(rand.Reader, ciphertext, nil) isok(t, err) assert(t, bytes.Compare(plaintext, computedPlaintext) == 0, "Plaintexts don't match") } func TestGenerateRSA1024(t *testing.T) { isDestructive() yubikey, closer, err := getYubikey(defaultPIN, defaultPUK) isok(t, err) defer closer() for _, slotId := range allSlots { isok(t, yubikey.Login()) isok(t, yubikey.Authenticate()) slot, err := yubikey.GenerateRSA(slotId, 1024) isok(t, err) assert(t, slot.PublicKey.(*rsa.PublicKey).N.BitLen() == 1024, "BitLen is wrong") } } func TestWriteSaveCycle(t *testing.T) { isDestructive() yubikey, closer, err := getYubikey(defaultPIN, defaultPUK) isok(t, err) defer closer() yubikey.Login() yubikey.Authenticate() isok(t, yubikey.SaveObject(0x5FCAFE, []byte("p̶͕͉̟ͅḁ̲̳̕u̪̬̯̗͎͡l̷͍͎̤̠t̥̗͞ag"))) whoami, err := yubikey.GetObject(0x5FCAFE) isok(t, err) assert(t, bytes.Compare(whoami, []byte("p̶͕͉̟ͅḁ̲̳̕u̪̬̯̗͎͡l̷͍͎̤̠t̥̗͞ag")) == 0, "get object returns good data") } func TestGenerateRSA2048(t *testing.T) { isDestructive() yubikey, closer, err := getYubikey(defaultPIN, defaultPUK) isok(t, err) defer closer() for _, slotId := range allSlots { isok(t, yubikey.Login()) isok(t, yubikey.Authenticate()) slot, err := yubikey.GenerateRSA(slotId, 2048) isok(t, err) assert(t, slot.PublicKey.(*rsa.PublicKey).N.BitLen() == 2048, "BitLen is wrong") } } func TestMain(m *testing.M) { isDestructive() os.Exit(m.Run()) } // vim: foldmethod=marker